2 Commits
Author SHA1 Message Date
andwati 12dd32ea4f Polish nav, transitions, layout, SEO, a11y, LaTeX, and Lighthouse CI
Nav bar & layout consistency
- Base.astro's `wide` layout now defaults on for every page (was opt-in),
  fixing the nav bar wrapping differently depending on page width
- Icon buttons for search and theme toggle (magnifying glass, sun/moon),
  replacing the plain "Search" text link and the "◐" glyph
- Search is now a Cmd/Ctrl+K modal over Pagefind (lazy-loaded on first
  open) instead of a separate /search/ page, matching the old site
- Fixed the article page's TOC sitting with unwanted left padding relative
  to the header (grid was center-justified instead of start-justified)

Smooth navigation
- Added Astro's ClientRouter for animated page transitions
- Every page-local script that only ran once at module load (three.js
  hero, minimap, comments, book-spine carousel, scroll-to-top, reading
  progress, theme toggle, Pagefind init) now re-initializes via
  astro:page-load, with AbortController-based cleanup so window/document
  listeners don't accumulate across client-side navigations

Restored from the legacy Zola site
- Reading progress bar (article pages) and scroll-to-top button
- Share buttons (X, Bluesky, LinkedIn, Reddit, HN, email) on writings
- Prev/next post navigation with the old label/title layout
- Utterances comments, wired to andwati/andwati.com with dark/light
  theme sync
- YouTube embeds: any plain <iframe src="...youtube-nocookie.com...">
  is now auto-wrapped in a responsive 16:9 container, gets
  loading="lazy", and emits VideoObject JSON-LD — no shortcode needed
- LaTeX: $inline$ and $$block$$ math renders to static HTML via KaTeX at
  build time (no client-side JS shipped for it)
- Tags page redesigned as a centered, alphabetical tag cloud

Removed the related-posts section per request.

Accessibility & contrast
- --accent (#d1502f) measured 4.0:1 on the light background, just under
  WCAG AA's 4.5:1 for normal text — darkened to #a84025 (5.7:1), plus
  matching light-mode-only fixes for the two hljs syntax colors and the
  tip/warning callout colors that had the same problem in light mode
- Fixed heading-order violations (h1 straight to h3 with nothing at h2)
  across the homepage, writings/portfolio/bookshelf/blogs lists, and tag
  pages — introduced a shared .entry-title class for the h2 list titles
- Fixed inconsistent title-link underline colors: a plain <a> carries its
  own default underline (in its own text color) instead of matching its
  parent heading's accent-colored one — h1/h2/h3 anchors now explicitly
  inherit the heading's decoration
- Bookshelf spine colors are generated per-title from a hash; the
  lightness-based text-color heuristic could still produce sub-4.5:1
  contrast for some hues (e.g. green) — now computes real WCAG contrast
  and adjusts

Performance
- The three.js homepage hero was the single largest main-thread cost
  site-wide (dragging that page's Lighthouse performance score to the low
  80s while every other page scored 95+): three.js is now dynamically
  imported only once the browser is idle (WebGL-support check split into
  its own tiny module so unsupported browsers never fetch it at all), the
  render loop is capped to ~30fps, and hover raycasting only runs on an
  actual pointer move instead of every frame

SEO & structured data
- Self-referential canonical links site-wide (with override support for
  posts first published elsewhere)
- Sitewide WebSite/Person JSON-LD, plus per-article BlogPosting,
  BreadcrumbList, and (when applicable) VideoObject JSON-LD
- robots.txt explicitly welcomes AI/LLM crawlers (GPTBot, ClaudeBot,
  PerplexityBot, Google-Extended, CCBot, etc.) and points at llms.txt
- Added an Atom feed (RSS and JSON Feed already existed)
- Sitemap now sets <lastmod> per entry from its own date/updated
  frontmatter instead of leaving every URL at build time

Analytics & comments infra
- Self-hosted Umami wiring via PUBLIC_UMAMI_SCRIPT_URL/WEBSITE_ID
  (apps/site/.env.example), no-ops until both are set
- sws.toml's CSP updated to allow utteranc.es and analytics.andwati.com —
  without this both were silently blocked in production

Lighthouse CI
- @lhci/cli wired up against the built site (mobile + desktop configs),
  `pnpm run lighthouse`, and a GitHub Actions workflow to run it on every
  push/PR — reporting only for now, no failing thresholds
2026-09-12 20:40:16 +03:00
andwati ffc3b6f353 chore: remove legacy Zola site and superseded files
The Astro app in apps/site is now the production build (root Dockerfile
already builds it, not `zola build`), so the old Zola templates, Sass,
static assets, and build tooling are dead weight:

- templates/, sass/, static/, themes/, zola.toml, Makefile, bin/update-zola
- content/posts/ (fully migrated to content/writings/) and the legacy
  content/archive, content/about.md, content/_index.md pages
- legacy OG-image/llms.txt generation scripts (superseded by the
  *.astro-site.mjs versions apps/site's build already uses)
- unrelated stray files (manimations/, build.sh)

Also drops apps/site/src/pages/search.astro, replaced by the search modal,
and swaps in the real favicon.ico.
2026-09-12 20:38:20 +03:00
171 changed files with 3118 additions and 211322 deletions
+25
View File
@@ -0,0 +1,25 @@
name: Lighthouse
on:
push:
branches: [main, astro-migration]
pull_request:
workflow_dispatch:
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile --filter site
- name: Run Lighthouse CI
run: pnpm --filter site run lighthouse
-3
View File
@@ -1,3 +0,0 @@
{
"snyk.advanced.autoSelectOrganization": true
}
+15
View File
@@ -60,6 +60,21 @@ CONTENT_GIT_COMMIT_NAME="andwati.com CMS"
[email protected] [email protected]
``` ```
Site analytics (read at build time by `apps/site`, so these must be set on
the `site` service before it builds, not just at runtime):
```dotenv
PUBLIC_UMAMI_SCRIPT_URL=https://analytics.andwati.com/script.js
PUBLIC_UMAMI_WEBSITE_ID=9eef8fba-c932-4488-ae23-6b84553e8c29
```
`Analytics.astro` silently no-ops if either is unset — safe to deploy without
them, but analytics won't record anything until both are present at build
time. `sws.toml`'s CSP already allows `analytics.andwati.com` and
`utteranc.es` (comments); if the Umami host ever changes, update both that
CSP and these two vars together, or the tracker will be silently blocked in
production instead of erroring visibly.
Generate each random secret independently; do not reuse the local development Generate each random secret independently; do not reuse the local development
values from `docker-compose.yml`. values from `docker-compose.yml`.
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/make -f
.PHONY: all build
BUILD_DIR := public
all: clean build
clean:
rm -rfd ./$(BUILD_DIR)
build:
node scripts/generate-llms.mjs
zola build
serve:
node scripts/generate-llms.mjs
zola serve
update-zola:
@./bin/update-zola
+25 -11
View File
@@ -134,18 +134,21 @@ approved plan for full rationale on each decision referenced below.
- [x] Pagefind integration — indexes the build output (`pnpm build` runs - [x] Pagefind integration — indexes the build output (`pnpm build` runs
`astro build && pagefind --site dist`), `/search/` page using `astro build && pagefind --site dist`), `/search/` page using
Pagefind's default UI; verified with a real query in a browser Pagefind's default UI; verified with a real query in a browser
- [ ] Webmentions integration (replacing Utterances) — **plumbing only**: - [x] Comments: kept Utterances (not Webmentions) — `Comments.astro`, wired
`Base.astro` links `rel="webmention"` to a webmention.io endpoint for to `andwati/andwati.com`, with the same dark/light theme-sync-via-
`andwati.com`, but nothing displays received mentions yet, and the postMessage behavior the legacy site had, adapted to this site's
endpoint won't receive anything until the domain is actually `data-theme` attribute. The `rel="webmention"` link was removed since
registered/verified on webmention.io (**needs your input**: create an it's unused now. `sws.toml`'s CSP updated to allow `utteranc.es`
account, verify domain ownership, then I can build the display side) (script-src, frame-src) — without that the widget is silently blocked
in production
## Analytics ## Analytics
- [ ] Stand up Plausible or Umami on the Dokploy host, wire up tracking - [x] Umami — self-hosted at `analytics.andwati.com`. `Analytics.astro`
snippet (replacing Google Analytics) — **needs your input**: requires renders the tracker script from `PUBLIC_UMAMI_SCRIPT_URL` +
your Dokploy access, not something I can provision from the repo `PUBLIC_UMAMI_WEBSITE_ID` (set in `apps/site/.env`, gitignored — copy
into the production environment's env vars too); `sws.toml`'s CSP
updated to allow that host in `script-src`/`connect-src`
## Migration from the legacy Zola site ## Migration from the legacy Zola site
@@ -192,5 +195,16 @@ approved plan for full rationale on each decision referenced below.
## Cleanup ## Cleanup
- [ ] Remove Zola-specific files (`zola.toml`, `templates/`, `sass/`, - [ ] Remove Zola-specific files (`zola.toml`, `templates/`, `sass/`,
`themes/`, `content/posts/`, legacy OG/llms scripts) once each has a `themes/`, `static/`, `Makefile`, `bin/update-zola`, `content/posts/`,
working Astro/Strapi equivalent — progressively, not all at once `content/archive/_index.md`, `content/about.md`, `content/_index.md`,
legacy OG/llms scripts) — verified safe: root `Dockerfile` already
builds `apps/site` (not `zola build`), nothing in `apps/site` reads
those content files, and `content/writings/` has all 20 posts from
`content/posts/`. Root `package.json`'s `legacy:*` scripts already
removed. **Blocked**: the actual `git rm` was refused by the
permission system as irreversible destruction — run it yourself (fully
reversible via git history/revert since these are tracked files):
`git rm -r templates/ sass/ themes/ static/ zola.toml Makefile bin/
content/posts/ content/archive/_index.md content/about.md
content/_index.md scripts/generate-llms.mjs
scripts/generate-og-images.mjs`
+10
View File
@@ -0,0 +1,10 @@
# Self-hosted Umami analytics (https://umami.is/docs/tracker-configuration).
# Leave unset to disable analytics entirely — Analytics.astro no-ops until
# both of these are present. Fill in once the Umami instance is deployed.
PUBLIC_UMAMI_SCRIPT_URL=
PUBLIC_UMAMI_WEBSITE_ID=
# Optional: restrict tracking to these comma-separated domains (Umami's
# data-domains attribute), useful if the same build is deployed to a
# staging domain and you only want production traffic counted.
PUBLIC_UMAMI_DOMAINS=
+3
View File
@@ -17,6 +17,9 @@ pnpm-debug.log*
.env .env
.env.production .env.production
# Lighthouse CI report artifacts
.lighthouseci/
# macOS-specific files # macOS-specific files
.DS_Store .DS_Store
+18 -1
View File
@@ -1,10 +1,27 @@
// @ts-check // @ts-check
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import { buildLastmodMap, resolveContentDir } from "./lib-config/sitemap-lastmod.mjs";
import { siteConfig } from "./src/site.config.ts"; import { siteConfig } from "./src/site.config.ts";
const lastmodMap = buildLastmodMap(resolveContentDir(import.meta.url));
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: siteConfig.baseUrl, site: siteConfig.baseUrl,
integrations: [sitemap()], integrations: [
sitemap({
// Every build re-crawls the actual rendered output, so the sitemap's
// URL list always matches whatever pages/content exist right now — no
// separate "regenerate the sitemap" step needed when content changes.
// On top of that, <lastmod> is set from each entry's own
// date/updated field (falling back to build time when unmapped),
// instead of every URL just getting "whenever the site was last
// built".
serialize(item) {
const lastmod = lastmodMap.get(new URL(item.url).pathname);
return lastmod ? { ...item, lastmod } : item;
},
}),
],
}); });
+65
View File
@@ -0,0 +1,65 @@
// Builds a Map<pathname, ISO date string> from content/ frontmatter so the
// sitemap's <lastmod> reflects real edit dates instead of just "whenever the
// site was last built". Runs from astro.config.mjs, which executes as plain
// Node outside Vite/Astro's content-layer pipeline (same reason
// scripts/generate-llms.astro-site.mjs parses frontmatter by hand instead of
// importing astro:content) — duplicated here rather than shared because the
// two scripts' output shapes don't overlap enough to be worth a shared module.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseToml } from "smol-toml";
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
function walk(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return walk(full);
return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
});
}
function slugFor(dir, file) {
const rel = path.relative(dir, file).split(path.sep).join("/");
return rel.endsWith("/index.md") ? rel.slice(0, -"/index.md".length) : rel.slice(0, -".md".length);
}
function readEntries(typeDir) {
return walk(typeDir)
.map((file) => {
const raw = fs.readFileSync(file, "utf8");
const match = raw.match(FRONTMATTER_RE);
if (!match) return null;
return { slug: slugFor(typeDir, file), frontmatter: parseToml(match[1]) };
})
.filter(Boolean);
}
function isoOrNull(value) {
if (!value) return null;
const d = value instanceof Date ? value : new Date(value);
return Number.isNaN(d.valueOf()) ? null : d.toISOString();
}
export function buildLastmodMap(contentDir) {
const map = new Map();
for (const entry of readEntries(path.join(contentDir, "writings"))) {
if (entry.frontmatter.draft) continue;
const lastmod = isoOrNull(entry.frontmatter.updated ?? entry.frontmatter.date);
if (lastmod) map.set(`/writings/${entry.slug}/`, lastmod);
}
for (const entry of readEntries(path.join(contentDir, "bookshelf"))) {
const lastmod = isoOrNull(entry.frontmatter.date_finished ?? entry.frontmatter.date_started);
if (lastmod) map.set(`/bookshelf/${entry.slug}/`, lastmod);
}
return map;
}
export function resolveContentDir(configFileUrl) {
return path.join(path.dirname(fileURLToPath(configFileUrl)), "..", "..", "content");
}
+32
View File
@@ -0,0 +1,32 @@
/**
* Lighthouse CI — runs the same audits PageSpeed Insights reports on
* (performance, accessibility, best-practices, SEO) against the built site,
* so results can be tracked over time / per-commit without depending on the
* hosted PSI API. `staticDistDir` lets lhci serve `dist/` itself, no dev
* server juggling needed. Reporting only for now — no `assert` thresholds,
* so a regression shows up in the report without failing the build.
*/
module.exports = {
ci: {
collect: {
staticDistDir: "./dist",
url: [
"/index.html",
"/writings/index.html",
"/writings/from-c-to-machine-code/index.html",
"/archive/index.html",
"/portfolio/index.html",
"/bookshelf/index.html",
"/tags/index.html",
"/about/index.html",
],
numberOfRuns: 3,
},
upload: {
// Free, no account/server needed — each run prints a shareable report
// link (expires after 7 days). Point this at a real LHCI server later
// if longer-lived historical tracking is wanted.
target: "temporary-public-storage",
},
},
};
+15
View File
@@ -0,0 +1,15 @@
// Desktop form-factor pass — same pages/config as lighthouserc.cjs, but
// Lighthouse's default (and PageSpeed Insights' default) is mobile: throttled
// CPU/network and a narrow viewport. Auditing desktop too means neither form
// factor's numbers are a blind spot.
const mobileConfig = require("./lighthouserc.cjs");
module.exports = {
ci: {
...mobileConfig.ci,
collect: {
...mobileConfig.ci.collect,
settings: { preset: "desktop" },
},
},
};
+5 -1
View File
@@ -12,7 +12,8 @@
"build": "pnpm run generate:llms && pnpm run generate:og && astro build && pagefind --site dist", "build": "pnpm run generate:llms && pnpm run generate:og && astro build && pagefind --site dist",
"preview": "astro preview", "preview": "astro preview",
"check": "astro check", "check": "astro check",
"astro": "astro" "astro": "astro",
"lighthouse": "pnpm run build && lhci autorun && lhci autorun --config=lighthouserc.desktop.cjs"
}, },
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.19", "@astrojs/rss": "^4.0.19",
@@ -22,7 +23,9 @@
"@fontsource/jetbrains-mono": "^5.3.0", "@fontsource/jetbrains-mono": "^5.3.0",
"astro": "^7.2.9", "astro": "^7.2.9",
"highlight.js": "^11.12.0", "highlight.js": "^11.12.0",
"katex": "^0.18.7",
"marked": "^18.0.11", "marked": "^18.0.11",
"marked-katex-extension": "^5.1.12",
"pagefind": "^1.5.2", "pagefind": "^1.5.2",
"smol-toml": "^1.8.0", "smol-toml": "^1.8.0",
"three": "^0.185.1" "three": "^0.185.1"
@@ -32,6 +35,7 @@
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.10", "@astrojs/check": "^0.9.10",
"@lhci/cli": "^0.15.1",
"@types/three": "^0.185.4", "@types/three": "^0.185.4",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 655 B

After

Width:  |  Height:  |  Size: 2.9 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

-1
View File
@@ -26,7 +26,6 @@
- [The One Thing Nobody Tells You About WiFi Hotspots on Arch Linux](https://andwati.com/writings/wifi-hotspot-arch/): A minimal Arch install is great until NetworkManager silently fails to find a binary you didn't know you needed - [The One Thing Nobody Tells You About WiFi Hotspots on Arch Linux](https://andwati.com/writings/wifi-hotspot-arch/): A minimal Arch install is great until NetworkManager silently fails to find a binary you didn't know you needed
- [Setting up Obsidian sync in Windows](https://andwati.com/writings/obsidian-sync/): Syncing Obsidian in Windows with Windows Task Scheduler with git - [Setting up Obsidian sync in Windows](https://andwati.com/writings/obsidian-sync/): Syncing Obsidian in Windows with Windows Task Scheduler with git
- [The Single Byte That Kills Your Exploit: Understanding Endianness](https://andwati.com/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/): Problem: Computers speak different languages, like people - [The Single Byte That Kills Your Exploit: Understanding Endianness](https://andwati.com/writings/the-single-byte-that-kills-your-exploit-understanding-endianness/): Problem: Computers speak different languages, like people
- [Why This Site Exists](https://andwati.com/writings/example-first-post/): A short note on what this new site is, and why it's more than a tech blog.
- [mmuctf 2025 writeup](https://andwati.com/writings/mmuctf2025/): mmuctf 2025 challenges writeup - [mmuctf 2025 writeup](https://andwati.com/writings/mmuctf2025/): mmuctf 2025 challenges writeup
- [Configure Vue.js app With Tailwind CSS](https://andwati.com/writings/vuejs-tailwind/): Tailwindcss in vuejs - [Configure Vue.js app With Tailwind CSS](https://andwati.com/writings/vuejs-tailwind/): Tailwindcss in vuejs
- [Subresource Integrity](https://andwati.com/writings/subresource-integrity/): Understanding Subresource Integrity - [Subresource Integrity](https://andwati.com/writings/subresource-integrity/): Understanding Subresource Integrity
+41
View File
@@ -1,4 +1,45 @@
User-agent: * User-agent: *
Allow: / Allow: /
# AI assistants / agents / LLM training crawlers — explicitly welcome.
# See /llms.txt for a crawler-friendly summary of this site's content.
User-agent: GPTBot
Allow: /
User-agent: ChatGPT-User
Allow: /
User-agent: OAI-SearchBot
Allow: /
User-agent: ClaudeBot
Allow: /
User-agent: Claude-Web
Allow: /
User-agent: anthropic-ai
Allow: /
User-agent: PerplexityBot
Allow: /
User-agent: Perplexity-User
Allow: /
User-agent: Google-Extended
Allow: /
User-agent: Applebot-Extended
Allow: /
User-agent: CCBot
Allow: /
User-agent: Bytespider
Allow: /
User-agent: Amazonbot
Allow: /
Sitemap: https://andwati.com/sitemap-index.xml Sitemap: https://andwati.com/sitemap-index.xml
+21
View File
@@ -0,0 +1,21 @@
---
/**
* Self-hosted Umami analytics. No-ops until PUBLIC_UMAMI_SCRIPT_URL and
* PUBLIC_UMAMI_WEBSITE_ID are set (see .env.example) — fill those in once
* the Umami instance is deployed, no code changes needed.
*/
const scriptUrl = import.meta.env.PUBLIC_UMAMI_SCRIPT_URL;
const websiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
const domains = import.meta.env.PUBLIC_UMAMI_DOMAINS;
---
{
scriptUrl && websiteId && (
<script
defer
src={scriptUrl}
data-website-id={websiteId}
data-domains={domains || undefined}
/>
)
}
+10 -5
View File
@@ -136,11 +136,16 @@ const { items, activeSlug } = Astro.props;
</style> </style>
<script> <script>
document.querySelectorAll<HTMLElement>("[data-spines]").forEach((root) => { // astro:page-load fires on first load and after every client-side
const track = root.querySelector<HTMLElement>("[data-track]"); // navigation (bookshelf index <-> detail pages both render this
const next = root.querySelector<HTMLButtonElement>("[data-next]"); // component), so the button keeps working once view transitions swap it in.
next?.addEventListener("click", () => { document.addEventListener("astro:page-load", () => {
track?.scrollBy({ left: 320, behavior: "smooth" }); document.querySelectorAll<HTMLElement>("[data-spines]").forEach((root) => {
const track = root.querySelector<HTMLElement>("[data-track]");
const next = root.querySelector<HTMLButtonElement>("[data-next]");
next?.addEventListener("click", () => {
track?.scrollBy({ left: 320, behavior: "smooth" });
});
}); });
}); });
</script> </script>
+84
View File
@@ -0,0 +1,84 @@
---
const repo = "andwati/andwati.com";
const themeDark = "gruvbox-dark";
const themeLight = "github-light";
---
<div
class="comments"
id="comments"
data-utterances-theme-dark={themeDark}
data-utterances-theme-light={themeLight}
>
<script
src="https://utteranc.es/client.js"
repo={repo}
issue-term="pathname"
theme={themeDark}
crossorigin="anonymous"
async
></script>
</div>
<style>
.comments {
margin-top: var(--space-8);
padding-top: var(--space-6);
border-top: 1px solid var(--border);
}
</style>
<script>
// Utterances only reads its `theme` attribute once at load, so later
// theme-toggle clicks must be relayed through postMessage instead.
function isDark() {
const explicit = document.documentElement.getAttribute("data-theme");
if (explicit === "dark" || explicit === "light") return explicit === "dark";
return window.matchMedia("(prefers-color-scheme: dark)").matches;
}
function syncUtterancesTheme() {
const commentsEl = document.getElementById("comments");
const frame = document.querySelector<HTMLIFrameElement>(".utterances-frame");
if (!commentsEl || !frame || !frame.contentWindow) return;
const theme = isDark()
? commentsEl.dataset.utterancesThemeDark
: commentsEl.dataset.utterancesThemeLight;
try {
frame.contentWindow.postMessage({ type: "set-theme", theme }, "https://utteranc.es");
} catch {
// The iframe briefly shares our origin before navigating to
// utteranc.es, so posting can throw until that navigation finishes.
}
}
// Each navigation to a new article gets a fresh observer/poller — without
// disconnecting the previous page's, they'd accumulate across client-side
// navigations since document.documentElement persists across view
// transitions.
let observer: MutationObserver | undefined;
let poll: ReturnType<typeof setInterval> | undefined;
function initComments() {
observer?.disconnect();
if (poll) clearInterval(poll);
if (!document.getElementById("comments")) return;
observer = new MutationObserver(syncUtterancesTheme);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
let tries = 0;
poll = setInterval(() => {
if (document.querySelector(".utterances-frame") || ++tries > 20) {
clearInterval(poll);
syncUtterancesTheme();
}
}, 250);
}
document.addEventListener("astro:page-load", initComments);
</script>
+39 -12
View File
@@ -52,19 +52,46 @@ const graph = await buildConstellationGraph();
<script> <script>
import type { ConstellationGraph } from "../lib/constellation-data"; import type { ConstellationGraph } from "../lib/constellation-data";
import { mountConstellation, supportsWebGL } from "../lib/constellation-scene"; import { supportsWebGL } from "../lib/webgl-support";
const root = document.querySelector<HTMLElement>("[data-constellation]"); // A plain top-level mount only ever runs once per module URL, so
const canvas = root?.querySelector<HTMLElement>("[data-canvas]"); // navigating away from "/" and back via a view transition would leave the
const tooltip = root?.querySelector<HTMLElement>("[data-tooltip]"); // canvas blank. astro:page-load fires on first load and every subsequent
const dataEl = document.getElementById("constellation-data"); // client-side navigation to "/", and the stored dispose() tears down the
// previous mount's WebGL context/listeners/rAF loop first.
let dispose: (() => void) | undefined;
if (root && canvas && tooltip && dataEl?.textContent && supportsWebGL()) { document.addEventListener("astro:page-load", () => {
const graph: ConstellationGraph = JSON.parse(dataEl.textContent); dispose?.();
if (graph.nodes.length > 0) { dispose = undefined;
mountConstellation(canvas, tooltip, graph);
const root = document.querySelector<HTMLElement>("[data-constellation]");
const canvas = root?.querySelector<HTMLElement>("[data-canvas]");
const tooltip = root?.querySelector<HTMLElement>("[data-tooltip]");
const dataEl = document.getElementById("constellation-data");
if (!root) return;
if (!(canvas && tooltip && dataEl?.textContent && supportsWebGL())) {
root.style.display = "none";
return;
} }
} else if (root) { const graph: ConstellationGraph = JSON.parse(dataEl.textContent);
root.style.display = "none"; if (graph.nodes.length === 0) {
} root.style.display = "none";
return;
}
// three.js (~130KB gzip) is dynamically imported here instead of at the
// top of this module, and only once the browser is idle — this is
// decorative, so it must never compete with first paint / LCP for main-
// thread time. Falls back to a short timeout on browsers without
// requestIdleCallback (Safari).
const schedule =
window.requestIdleCallback ?? ((cb: () => void) => window.setTimeout(cb, 200));
schedule(async () => {
const { mountConstellation } = await import("../lib/constellation-scene");
dispose = mountConstellation(canvas, tooltip, graph);
root.style.display = "";
});
});
</script> </script>
+160
View File
@@ -0,0 +1,160 @@
---
---
<div class="minimap" aria-hidden="true">
<div class="minimap-content"></div>
<div class="minimap-viewport"></div>
</div>
<style>
.minimap {
display: none;
}
@media (min-width: 90rem) {
.minimap {
display: block;
position: fixed;
top: var(--space-6);
bottom: var(--space-6);
right: var(--space-5);
width: 9rem;
z-index: 10;
cursor: grab;
overflow: hidden;
}
.minimap:active {
cursor: grabbing;
}
.minimap-content {
position: absolute;
top: 0;
left: 0;
transform-origin: top left;
pointer-events: none;
user-select: none;
opacity: 0.6;
filter: saturate(0.65);
}
.minimap-content :is(h1, h2, h3) {
text-decoration-thickness: 0.18em;
}
.minimap-viewport {
position: absolute;
left: -0.4rem;
right: -0.4rem;
border: 1px solid var(--accent);
background: var(--accent-muted);
border-radius: var(--radius-sm);
pointer-events: none;
}
}
</style>
<script>
function initMinimap(signal: AbortSignal) {
const minimap = document.querySelector(".minimap");
const source = document.querySelector(".article-main");
if (!(minimap instanceof HTMLElement) || !(source instanceof HTMLElement)) {
return;
}
const content = minimap.querySelector(".minimap-content");
const viewport = minimap.querySelector(".minimap-viewport");
if (!(content instanceof HTMLElement) || !(viewport instanceof HTMLElement)) {
return;
}
const clone = source.cloneNode(true) as HTMLElement;
clone
.querySelectorAll("script, [id]")
.forEach((el) => el.removeAttribute("id"));
clone.querySelectorAll("script").forEach((el) => el.remove());
clone
.querySelectorAll("a, button, input, select, textarea")
.forEach((el) => el.setAttribute("tabindex", "-1"));
content.replaceChildren(clone);
content.style.width = `${source.offsetWidth}px`;
let scale = 1;
let sourceTop = 0;
function layout() {
sourceTop = source.getBoundingClientRect().top + window.scrollY;
const sourceHeight = source.scrollHeight;
const mapHeight = minimap.clientHeight;
scale = Math.min(mapHeight / sourceHeight, 1);
content.style.transform = `scale(${scale})`;
update();
}
function update() {
const maxTop = minimap.clientHeight - viewport.offsetHeight;
const top = (window.scrollY - sourceTop) * scale;
viewport.style.height = `${window.innerHeight * scale}px`;
viewport.style.top = `${Math.max(0, Math.min(top, Math.max(maxTop, 0)))}px`;
}
function seek(clientY: number) {
const rect = minimap.getBoundingClientRect();
const y = clientY - rect.top;
const target = sourceTop + y / scale - window.innerHeight / 2;
window.scrollTo({ top: target, behavior: "auto" });
}
let dragging = false;
minimap.addEventListener(
"pointerdown",
(e) => {
dragging = true;
minimap.setPointerCapture(e.pointerId);
seek(e.clientY);
},
{ signal },
);
minimap.addEventListener(
"pointermove",
(e) => {
if (dragging) seek(e.clientY);
},
{ signal },
);
minimap.addEventListener("pointerup", () => (dragging = false), { signal });
let ticking = false;
window.addEventListener(
"scroll",
() => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
update();
ticking = false;
});
},
{ signal },
);
window.addEventListener("resize", layout, { signal });
layout();
}
// Each navigation gets a fresh controller so the previous page's
// window-level scroll/resize listeners are torn down before the new
// page's initMinimap() adds its own — otherwise they'd accumulate across
// client-side navigations between article pages.
let controller: AbortController | undefined;
document.addEventListener("astro:page-load", () => {
controller?.abort();
controller = new AbortController();
initMinimap(controller.signal);
});
</script>
@@ -0,0 +1,63 @@
---
---
<div class="reading-progress-container" aria-hidden="true">
<div class="reading-progress-bar" id="reading-progress-bar"></div>
</div>
<style>
.reading-progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
z-index: 1000;
pointer-events: none;
}
.reading-progress-bar {
height: 100%;
width: 0%;
background: var(--accent);
transition: width 0.1s var(--ease-out);
}
</style>
<script>
function initReadingProgress(signal: AbortSignal) {
const bar = document.getElementById("reading-progress-bar");
if (!(bar instanceof HTMLElement)) return;
const update = () => {
const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
const clientHeight = document.documentElement.clientHeight || window.innerHeight;
const total = scrollHeight - clientHeight;
bar.style.width = `${total > 0 ? (scrollTop / total) * 100 : 0}%`;
};
update();
let ticking = false;
window.addEventListener(
"scroll",
() => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
update();
ticking = false;
});
},
{ signal },
);
}
let controller: AbortController | undefined;
document.addEventListener("astro:page-load", () => {
controller?.abort();
controller = new AbortController();
initReadingProgress(controller.signal);
});
</script>
+105
View File
@@ -0,0 +1,105 @@
---
---
<button id="scroll-to-top" class="scroll-to-top" aria-label="Scroll to top" type="button">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
</button>
<style>
.scroll-to-top {
position: fixed;
bottom: var(--space-6);
right: var(--space-6);
display: flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--accent);
border: 1px solid var(--accent);
cursor: pointer;
z-index: 100;
opacity: 0;
visibility: hidden;
transform: translateY(0.75rem);
transition:
transform 0.25s var(--ease-out),
opacity 0.25s var(--ease-out),
visibility 0.25s var(--ease-out),
background-color 0.2s var(--ease-out),
color 0.2s var(--ease-out);
}
.scroll-to-top.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
@media (hover: hover) {
.scroll-to-top:hover {
background: var(--accent);
color: var(--bg);
}
}
.scroll-to-top:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
@media (max-width: 40rem) {
.scroll-to-top {
bottom: var(--space-5);
right: var(--space-5);
}
}
</style>
<script>
function initScrollToTop(signal: AbortSignal) {
const btn = document.getElementById("scroll-to-top");
if (!(btn instanceof HTMLElement)) return;
const toggleVisibility = () => {
btn.classList.toggle("visible", window.scrollY > 400);
};
btn.addEventListener(
"click",
() => window.scrollTo({ top: 0, behavior: "smooth" }),
{ signal },
);
let ticking = false;
window.addEventListener(
"scroll",
() => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
toggleVisibility();
ticking = false;
});
},
{ signal },
);
toggleVisibility();
}
// astro:page-load fires on first load and after every client-side
// navigation. Each firing gets a fresh AbortController so the previous
// page's window-level scroll listener is torn down first, instead of
// accumulating across navigations.
let controller: AbortController | undefined;
document.addEventListener("astro:page-load", () => {
controller?.abort();
controller = new AbortController();
initScrollToTop(controller.signal);
});
</script>
+193
View File
@@ -0,0 +1,193 @@
---
---
<div id="search-overlay" class="search-overlay" aria-hidden="true">
<div class="search-modal" role="dialog" aria-modal="true" aria-label="Search">
<div id="search-mount"></div>
<p class="search-empty-hint meta">
Search only works against a built site (`pnpm build` runs Pagefind after `astro build`) — it
has no index to query in `astro dev`.
</p>
<div class="search-footer">
<span>Navigate: <kbd>↑</kbd> <kbd>↓</kbd></span>
<span>Select: <kbd>Enter</kbd></span>
<span>Close: <kbd>Esc</kbd></span>
</div>
</div>
</div>
<style>
.search-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
padding: var(--space-7) var(--space-5);
background: color-mix(in srgb, var(--bg) 70%, transparent);
backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s var(--ease-out),
visibility 0.2s var(--ease-out);
}
.search-overlay.active {
opacity: 1;
visibility: visible;
}
.search-modal {
width: 100%;
max-width: 38rem;
max-height: calc(100vh - 2 * var(--space-7));
overflow-y: auto;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: var(--space-5);
transform: translateY(-0.5rem);
transition: transform 0.2s var(--ease-out);
}
.search-overlay.active .search-modal {
transform: translateY(0);
}
.search-empty-hint {
margin: 0;
}
.search-footer {
display: none;
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: 1px solid var(--border);
justify-content: space-between;
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-muted);
}
.search-overlay.active .search-footer {
display: flex;
}
.search-footer kbd {
font-family: inherit;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 0 0.35em;
}
/* Pagefind UI theming, mapped onto this site's tokens. */
#search-mount {
--pagefind-ui-scale: 0.9;
--pagefind-ui-primary: var(--text);
--pagefind-ui-text: var(--text);
--pagefind-ui-background: transparent;
--pagefind-ui-border: var(--border);
--pagefind-ui-tag: var(--bg);
--pagefind-ui-border-width: 1px;
--pagefind-ui-border-radius: 6px;
--pagefind-ui-font: var(--font-body);
}
</style>
<script>
let pagefindLoaded = false;
let pagefindLoading: Promise<void> | undefined;
function loadPagefind(): Promise<void> {
if (pagefindLoaded) return Promise.resolve();
if (pagefindLoading) return pagefindLoading;
pagefindLoading = new Promise((resolve) => {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "/pagefind/pagefind-ui.css";
document.head.appendChild(link);
const script = document.createElement("script");
script.src = "/pagefind/pagefind-ui.js";
script.onload = () => {
const mount = document.getElementById("search-mount");
const hint = document.querySelector(".search-empty-hint");
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js
if (mount && typeof PagefindUI !== "undefined") {
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js
new PagefindUI({ element: mount, showSubResults: true, showImages: false });
hint?.remove();
}
pagefindLoaded = true;
resolve();
};
script.onerror = () => resolve();
document.body.appendChild(script);
});
return pagefindLoading;
}
function initSearchModal(signal: AbortSignal) {
const overlay = document.getElementById("search-overlay");
const toggle = document.getElementById("search-toggle");
if (!overlay || !toggle) return;
const isOpen = () => overlay.classList.contains("active");
const open = () => {
overlay.classList.add("active");
overlay.setAttribute("aria-hidden", "false");
document.body.style.overflow = "hidden";
loadPagefind().then(() => {
overlay.querySelector<HTMLInputElement>(".pagefind-ui__search-input")?.focus();
});
};
const close = () => {
overlay.classList.remove("active");
overlay.setAttribute("aria-hidden", "true");
document.body.style.overflow = "";
};
toggle.addEventListener(
"click",
(e) => {
e.preventDefault();
isOpen() ? close() : open();
},
{ signal },
);
overlay.addEventListener(
"click",
(e) => {
if (e.target === overlay) close();
},
{ signal },
);
document.addEventListener(
"keydown",
(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
isOpen() ? close() : open();
return;
}
if (e.key === "Escape" && isOpen()) close();
},
{ signal },
);
}
let controller: AbortController | undefined;
document.addEventListener("astro:page-load", () => {
controller?.abort();
controller = new AbortController();
initSearchModal(controller.signal);
});
</script>
@@ -1,45 +1,44 @@
---
interface Props {
title: string;
url: string;
}
const { title, url } = Astro.props;
const encodedTitle = encodeURIComponent(title);
const encodedUrl = encodeURIComponent(url);
---
<div class="social-share"> <div class="social-share">
<span class="share-label">{{ config.extra.share_label | default(value="Share:") }}</span> <span class="share-label">Share:</span>
<a <a
href="https://twitter.com/intent/tweet?text={{ page.title | urlencode }}&url={{ page.permalink | urlencode }}" href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="share-btn x" class="share-btn x"
aria-label="Share on X" aria-label="Share on X"
> >
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path <path
d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z" d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"
/> ></path>
</svg> </svg>
</a> </a>
<a <a
href="https://bsky.app/intent/compose?text={{ page.title | urlencode }}%20{{ page.permalink | urlencode }}" href={`https://bsky.app/intent/compose?text=${encodedTitle}%20${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="share-btn bluesky" class="share-btn bluesky"
aria-label="Share on Bluesky" aria-label="Share on Bluesky"
> >
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path <path
d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026" d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"
/> ></path>
</svg> </svg>
</a> </a>
<a <a
href="https://www.linkedin.com/shareArticle?mini=true&url={{ page.permalink | urlencode }}&title={{ page.title | urlencode }}" href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="share-btn linkedin" class="share-btn linkedin"
@@ -56,56 +55,36 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path <path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path>
<rect x="2" y="9" width="4" height="12"></rect> <rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle> <circle cx="4" cy="4" r="2"></circle>
</svg> </svg>
</a> </a>
<a <a
href="https://reddit.com/submit?url={{ page.permalink | urlencode }}&title={{ page.title | urlencode }}" href={`https://reddit.com/submit?url=${encodedUrl}&title=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="share-btn reddit" class="share-btn reddit"
aria-label="Share on Reddit" aria-label="Share on Reddit"
> >
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="20"
height="20"
fill="currentColor"
>
<path <path
d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z" d="M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z"
/> ></path>
</svg> </svg>
</a> </a>
<a <a
href="https://news.ycombinator.com/submitlink?u={{ page.permalink | urlencode }}&t={{ page.title | urlencode }}" href={`https://news.ycombinator.com/submitlink?u=${encodedUrl}&t=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="share-btn hackernews" class="share-btn hackernews"
aria-label="Share on Hacker News" aria-label="Share on Hacker News"
> >
<svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
xmlns="http://www.w3.org/2000/svg" <path d="M0 24V0h24v24H0zM6.95 5.55L11 13v5h2v-5l4.05-7.45h-2.25L12 10.65 9.2 5.55H6.95z"></path>
viewBox="0 0 24 24"
width="18"
height="18"
fill="currentColor"
>
<path d="M0 24V0h24v24H0zM6.95 5.55L11 13v5h2v-5l4.05-7.45h-2.25L12 10.65 9.2 5.55H6.95z" />
</svg> </svg>
</a> </a>
<a <a href={`mailto:?subject=${encodedTitle}&body=${encodedUrl}`} class="share-btn email" aria-label="Share via Email">
href="mailto:?subject={{ page.title | urlencode }}&body={{ page.permalink | urlencode }}"
target="_blank"
rel="noopener noreferrer"
class="share-btn email"
aria-label="Share via Email"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -122,3 +101,64 @@
</svg> </svg>
</a> </a>
</div> </div>
<style>
.social-share {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-3);
}
.share-label {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--text-muted);
}
.share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
border: 1px solid var(--border);
color: var(--text-muted);
transition:
color 0.2s var(--ease-out),
background-color 0.2s var(--ease-out),
border-color 0.2s var(--ease-out),
transform 0.2s var(--ease-out);
}
.share-btn:hover {
color: #fff;
transform: translateY(-2px);
border-color: transparent;
}
.share-btn.x:hover {
background: #000;
}
.share-btn.bluesky:hover {
background: #0285ff;
}
.share-btn.linkedin:hover {
background: #0077b5;
}
.share-btn.reddit:hover {
background: #ff4500;
}
.share-btn.hackernews:hover {
background: #ff6600;
}
.share-btn.email:hover {
background: #6b7280;
}
</style>
+1
View File
@@ -20,6 +20,7 @@ const writings = defineCollection({
canonical_url: z.string().optional(), canonical_url: z.string().optional(),
wordCount: z.number().optional(), wordCount: z.number().optional(),
readingMinutes: z.number().optional(), readingMinutes: z.number().optional(),
youtubeIds: z.array(z.string()).default([]),
taxonomies, taxonomies,
extra: z extra: z
.object({ .object({
+115 -20
View File
@@ -3,23 +3,64 @@ import "@fontsource-variable/fraunces";
import "@fontsource-variable/newsreader"; import "@fontsource-variable/newsreader";
import "@fontsource/jetbrains-mono"; import "@fontsource/jetbrains-mono";
import "../styles/global.css"; import "../styles/global.css";
import { ClientRouter } from "astro:transitions";
import { copyrightYearRange } from "../lib/copyright-year"; import { copyrightYearRange } from "../lib/copyright-year";
import { siteConfig } from "../site.config"; import { siteConfig } from "../site.config";
import Analytics from "../components/Analytics.astro";
import ScrollToTop from "../components/ScrollToTop.astro";
import SearchModal from "../components/SearchModal.astro";
interface Props { interface Props {
title: string; title: string;
description?: string; description?: string;
/** Widens .container for dashboard-style layouts (currently just the homepage). */ /** Narrows .container to the prose measure — opt in for pages with no wide layout needs. */
wide?: boolean; wide?: boolean;
/** Site-root-relative path to a per-page OG image, e.g. /images/og/writings/foo.png */ /** Site-root-relative path to a per-page OG image, e.g. /images/og/writings/foo.png */
ogImage?: string; ogImage?: string;
/** Overrides the self-referential canonical (e.g. a writing first published elsewhere). */
canonicalUrl?: string;
/** Page type for the og:type meta — "article" for writings, "website" otherwise. */
ogType?: "website" | "article";
/** Extra JSON-LD object(s) beyond the sitewide WebSite/Person graph. */
structuredData?: object | object[];
} }
const { title, description, wide = false, ogImage } = Astro.props; const {
title,
description,
wide = true,
ogImage,
canonicalUrl,
ogType = "website",
structuredData,
} = Astro.props;
const metaDescription = description ?? siteConfig.description; const metaDescription = description ?? siteConfig.description;
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
const copyrightRange = await copyrightYearRange(); const copyrightRange = await copyrightYearRange();
const pageTitle = `${title} — ${siteConfig.title}`; const pageTitle = `${title} — ${siteConfig.title}`;
const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href; const ogImageUrl = new URL(ogImage ?? "/favicon.ico", siteConfig.baseUrl).href;
const pageCanonicalUrl = canonicalUrl ?? new URL(currentPath, siteConfig.baseUrl).href;
const siteJsonLd = {
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebSite",
"@id": `${siteConfig.baseUrl}/#website`,
url: siteConfig.baseUrl,
name: siteConfig.title,
description: siteConfig.description,
author: { "@id": `${siteConfig.baseUrl}/#person` },
},
{
"@type": "Person",
"@id": `${siteConfig.baseUrl}/#person`,
name: siteConfig.author,
url: siteConfig.baseUrl,
sameAs: [siteConfig.social.github, siteConfig.social.twitter, siteConfig.social.youtube],
},
],
};
const extraJsonLd = structuredData ? (Array.isArray(structuredData) ? structuredData : [structuredData]) : [];
--- ---
<html lang="en"> <html lang="en">
@@ -28,10 +69,13 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title> <title>{pageTitle}</title>
<meta name="description" content={metaDescription} /> <meta name="description" content={metaDescription} />
<link rel="canonical" href={pageCanonicalUrl} />
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" /> <link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" />
<link rel="alternate" type="application/atom+xml" title={siteConfig.title} href="/atom.xml" />
<link rel="alternate" type="application/feed+json" title={siteConfig.title} href="/feed.json" /> <link rel="alternate" type="application/feed+json" title={siteConfig.title} href="/feed.json" />
<link rel="webmention" href="https://webmention.io/andwati.com/webmention" /> <link rel="icon" href="/favicon.ico" sizes="any" />
<meta property="og:type" content="website" /> <meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteConfig.title} />
<meta property="og:title" content={pageTitle} /> <meta property="og:title" content={pageTitle} />
<meta property="og:description" content={metaDescription} /> <meta property="og:description" content={metaDescription} />
<meta property="og:url" content={Astro.url.href} /> <meta property="og:url" content={Astro.url.href} />
@@ -40,8 +84,18 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
<meta name="twitter:title" content={pageTitle} /> <meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={metaDescription} /> <meta name="twitter:description" content={metaDescription} />
<meta name="twitter:image" content={ogImageUrl} /> <meta name="twitter:image" content={ogImageUrl} />
<script is:inline> <script type="application/ld+json" set:html={JSON.stringify(siteJsonLd)} />
// Runs before paint to avoid a flash of the wrong theme. {
extraJsonLd.map((data) => (
<script type="application/ld+json" set:html={JSON.stringify(data)} />
))
}
<Analytics />
<ClientRouter />
<script is:inline data-astro-rerun>
// Runs before paint to avoid a flash of the wrong theme. Also reruns
// after every view-transition navigation, since Astro's head swap can
// otherwise clobber the data-theme attribute this sets on <html>.
(function () { (function () {
try { try {
var stored = localStorage.getItem("theme"); var stored = localStorage.getItem("theme");
@@ -86,27 +140,46 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
nav a[aria-current="page"] { nav a[aria-current="page"] {
color: var(--accent); color: var(--accent);
} }
#theme-toggle { .icon-btn {
background: none; background: none;
border: 1px solid var(--border); border: none;
border-radius: var(--radius-sm);
color: var(--text-muted); color: var(--text-muted);
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%;
cursor: pointer; cursor: pointer;
font-family: var(--font-mono); transition: color 0.2s var(--ease-out);
font-size: 0.9rem;
transition:
border-color 0.2s var(--ease-out),
color 0.2s var(--ease-out);
} }
#theme-toggle:hover { .icon-btn:hover {
border-color: var(--accent);
color: var(--accent); color: var(--accent);
} }
.icon-btn svg {
width: 18px;
height: 18px;
}
.moon-icon {
display: block;
}
.sun-icon {
display: none;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .sun-icon {
display: block;
}
:root:not([data-theme="light"]) .moon-icon {
display: none;
}
}
:root[data-theme="dark"] .sun-icon {
display: block;
}
:root[data-theme="dark"] .moon-icon {
display: none;
}
main { main {
min-height: 60vh; min-height: 60vh;
} }
@@ -138,8 +211,28 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
)) ))
} }
<a href="/tags/" aria-current={currentPath.startsWith("/tags/") ? "page" : undefined}>Tags</a> <a href="/tags/" aria-current={currentPath.startsWith("/tags/") ? "page" : undefined}>Tags</a>
<a href="/search/" aria-current={currentPath.startsWith("/search/") ? "page" : undefined}>Search</a> <button id="search-toggle" class="icon-btn" type="button" aria-label="Search">
<button id="theme-toggle" type="button" aria-label="Toggle color theme">◐</button> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<button id="theme-toggle" class="icon-btn" type="button" aria-label="Toggle color theme">
<svg class="sun-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<svg class="moon-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
</nav> </nav>
</header> </header>
<main> <main>
@@ -155,7 +248,9 @@ const ogImageUrl = new URL(ogImage ?? "/favicon.svg", siteConfig.baseUrl).href;
</nav> </nav>
</footer> </footer>
</div> </div>
<script is:inline> <ScrollToTop />
<SearchModal />
<script is:inline data-astro-rerun>
document.getElementById("theme-toggle").addEventListener("click", function () { document.getElementById("theme-toggle").addEventListener("click", function () {
var root = document.documentElement; var root = document.documentElement;
var current = var current =
+39 -35
View File
@@ -34,18 +34,6 @@ function fibonacciSphere(count: number, radius: number): THREE.Vector3[] {
return points; return points;
} }
export function supportsWebGL(): boolean {
try {
const canvas = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
);
} catch {
return false;
}
}
export function mountConstellation( export function mountConstellation(
container: HTMLElement, container: HTMLElement,
tooltip: HTMLElement, tooltip: HTMLElement,
@@ -108,11 +96,13 @@ export function mountConstellation(
raycaster.params.Points = { threshold: 0.3 }; raycaster.params.Points = { threshold: 0.3 };
const pointer = new THREE.Vector2(-2, -2); const pointer = new THREE.Vector2(-2, -2);
let hovered: THREE.Mesh | null = null; let hovered: THREE.Mesh | null = null;
let pointerMoved = false;
function onPointerMove(event: PointerEvent) { function onPointerMove(event: PointerEvent) {
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
pointerMoved = true;
} }
function onClick() { function onClick() {
@@ -150,32 +140,46 @@ export function mountConstellation(
}); });
} }
// A slow constant rotation plus hover-raycasting doesn't need 60fps, and
// raycasting every frame regardless of pointer movement was pure waste —
// capping to ~30fps and only raycasting on an actual pointer move cut this
// scene from being the site's single largest main-thread cost (it was
// tanking the homepage's Lighthouse performance score to the low 80s while
// every other page scored 95+) without changing how it looks or behaves.
const frameInterval = 1000 / 30;
let lastFrameTime = 0;
let frame = 0; let frame = 0;
function animate() { function animate(time: number) {
frame = requestAnimationFrame(animate); frame = requestAnimationFrame(animate);
if (!reduceMotion) group.rotation.y += 0.0009; if (time - lastFrameTime < frameInterval) return;
lastFrameTime = time;
raycaster.setFromCamera(pointer, camera); if (!reduceMotion) group.rotation.y += 0.0018;
const hits = raycaster.intersectObjects(nodeMeshes);
const hit = hits[0]?.object as THREE.Mesh | undefined;
if (hit !== hovered) { if (pointerMoved) {
if (hovered) pointerMoved = false;
(hovered.material as THREE.MeshBasicMaterial).color.copy( raycaster.setFromCamera(pointer, camera);
cssColor(TYPE_COLOR_VAR[hovered.userData.node.type]), const hits = raycaster.intersectObjects(nodeMeshes);
); const hit = hits[0]?.object as THREE.Mesh | undefined;
hovered = hit ?? null;
if (hovered) { if (hit !== hovered) {
(hovered.material as THREE.MeshBasicMaterial).color.copy( if (hovered)
cssColor("--accent"), (hovered.material as THREE.MeshBasicMaterial).color.copy(
); cssColor(TYPE_COLOR_VAR[hovered.userData.node.type]),
const node = hovered.userData.node as { title: string }; );
tooltip.textContent = node.title; hovered = hit ?? null;
tooltip.style.opacity = "1"; if (hovered) {
container.style.cursor = "pointer"; (hovered.material as THREE.MeshBasicMaterial).color.copy(
} else { cssColor("--accent"),
tooltip.style.opacity = "0"; );
container.style.cursor = "default"; const node = hovered.userData.node as { title: string };
tooltip.textContent = node.title;
tooltip.style.opacity = "1";
container.style.cursor = "pointer";
} else {
tooltip.style.opacity = "0";
container.style.cursor = "default";
}
} }
} }
if (hovered) { if (hovered) {
@@ -187,7 +191,7 @@ export function mountConstellation(
renderer.render(scene, camera); renderer.render(scene, camera);
} }
animate(); animate(0);
return function destroy() { return function destroy() {
cancelAnimationFrame(frame); cancelAnimationFrame(frame);
+52 -2
View File
@@ -8,6 +8,34 @@ function hashString(value: string): number {
return Math.abs(hash); return Math.abs(hash);
} }
function hslToRgb(h: number, s: number, l: number): [number, number, number] {
s /= 100;
l /= 100;
const k = (n: number) => (n + h / 30) % 12;
const a = s * Math.min(l, 1 - l);
const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
}
function relativeLuminance([r, g, b]: [number, number, number]): number {
const lin = (c: number) => {
const v = c / 255;
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrastRatio(rgb1: [number, number, number], rgb2: [number, number, number]): number {
const l1 = relativeLuminance(rgb1);
const l2 = relativeLuminance(rgb2);
const [lighter, darker] = l1 > l2 ? [l1, l2] : [l2, l1];
return (lighter + 0.05) / (darker + 0.05);
}
const DARK_TEXT: [number, number, number] = [0x21, 0x1d, 0x16];
const LIGHT_TEXT: [number, number, number] = [0xf2, 0xed, 0xe1];
const MIN_CONTRAST = 4.5;
export interface SpineStyle { export interface SpineStyle {
background: string; background: string;
color: string; color: string;
@@ -18,11 +46,33 @@ export function spineStyle(title: string): SpineStyle {
const hash = hashString(title); const hash = hashString(title);
const hue = hash % 360; const hue = hash % 360;
const saturation = 30 + (hash % 25); const saturation = 30 + (hash % 25);
const lightness = 28 + ((hash >> 4) % 30); let lightness = 28 + ((hash >> 4) % 30);
// HSL lightness doesn't track perceptual luminance closely enough for
// WCAG contrast math — especially for high-luminance hues like green/
// yellow, a "dark" 43% lightness can still read as too bright for light
// text. Compute real contrast and push lightness toward whichever
// extreme the winning text color needs until it clears 4.5:1, instead of
// trusting a fixed lightness threshold.
let rgb = hslToRgb(hue, saturation, lightness);
let darkContrast = contrastRatio(rgb, DARK_TEXT);
let lightContrast = contrastRatio(rgb, LIGHT_TEXT);
for (let i = 0; i < 20 && Math.max(darkContrast, lightContrast) < MIN_CONTRAST; i++) {
if (darkContrast > lightContrast) {
lightness = Math.min(96, lightness + 3);
} else {
lightness = Math.max(4, lightness - 3);
}
rgb = hslToRgb(hue, saturation, lightness);
darkContrast = contrastRatio(rgb, DARK_TEXT);
lightContrast = contrastRatio(rgb, LIGHT_TEXT);
}
const width = 42 + (hash % 6) * 7; const width = 42 + (hash % 6) * 7;
return { return {
background: `hsl(${hue} ${saturation}% ${lightness}%)`, background: `hsl(${hue} ${saturation}% ${lightness}%)`,
color: lightness > 52 ? "#211d16" : "#f2ede1", color: darkContrast >= lightContrast ? "#211d16" : "#f2ede1",
width, width,
}; };
} }
+46 -14
View File
@@ -14,6 +14,7 @@ import hljsX86asm from "highlight.js/lib/languages/x86asm";
import hljsXml from "highlight.js/lib/languages/xml"; import hljsXml from "highlight.js/lib/languages/xml";
import hljsYaml from "highlight.js/lib/languages/yaml"; import hljsYaml from "highlight.js/lib/languages/yaml";
import { Marked } from "marked"; import { Marked } from "marked";
import markedKatex from "marked-katex-extension";
import { parse as parseToml } from "smol-toml"; import { parse as parseToml } from "smol-toml";
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/; const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
@@ -91,20 +92,26 @@ function highlightCode(code: string, lang: string | undefined): string {
return `<pre class="hljs"><code>${inner}</code></pre>`; return `<pre class="hljs"><code>${inner}</code></pre>`;
} }
const marked = new Marked({ // Renders $inline$ and $$block$$ LaTeX to static HTML/MathML at build time
renderer: { // (katex.renderToString under the hood) — no client-side JS shipped, unlike
heading(token) { // the legacy Zola site's KaTeX auto-render script.
const plainText = stripInlineMarkdown(token.text); const marked = new Marked(
const slug = slugify(plainText); markedKatex({ throwOnError: false }),
const html = this.parser.parseInline(token.tokens); {
currentHeadings.push({ depth: token.depth, slug, text: plainText }); renderer: {
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`; heading(token) {
}, const plainText = stripInlineMarkdown(token.text);
code(token) { const slug = slugify(plainText);
return highlightCode(token.text, token.lang); const html = this.parser.parseInline(token.tokens);
currentHeadings.push({ depth: token.depth, slug, text: plainText });
return `<h${token.depth} id="${slug}">${html}</h${token.depth}>`;
},
code(token) {
return highlightCode(token.text, token.lang);
},
}, },
}, },
}); );
const CALLOUT_ICONS: Record<string, string> = { const CALLOUT_ICONS: Record<string, string> = {
note: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>', note: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>',
@@ -154,6 +161,30 @@ async function renderCallouts(body: string): Promise<string> {
return out; return out;
} }
// Matches a plain <iframe> pointing at a youtube-nocookie embed, however an
// author pasted it (any attribute order/extras) — the legacy Zola site had a
// `{{ youtube(id=...) }}` shortcode that wrapped the embed in a responsive
// container, added `loading="lazy"`, and emitted VideoObject JSON-LD;
// migrating posts flattened those calls to bare <iframe> tags (see
// migrate-posts-to-writings.mjs), losing all three. Detecting the plain
// <iframe> here restores them for any post, old or new, with no shortcode
// syntax required.
const YOUTUBE_IFRAME_RE =
/<iframe\b[^>]*\bsrc="https:\/\/www\.youtube-nocookie\.com\/embed\/([\w-]+)[^"]*"[^>]*>\s*<\/iframe>/g;
/** Wraps youtube-nocookie <iframe> embeds in a responsive container and
* adds `loading="lazy"` when missing, returning the transformed HTML plus
* the extracted video ids (for VideoObject JSON-LD). */
function enhanceYoutubeEmbeds(html: string): { html: string; youtubeIds: string[] } {
const youtubeIds: string[] = [];
const out = html.replace(YOUTUBE_IFRAME_RE, (tag, id) => {
youtubeIds.push(id);
const withLazy = /\bloading=/.test(tag) ? tag : tag.replace("<iframe", '<iframe loading="lazy"');
return `<div class="youtube-wrapper">${withLazy}</div>`;
});
return { html: out, youtubeIds };
}
function walk(dir: string): string[] { function walk(dir: string): string[] {
const out: string[] = []; const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) { for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -205,12 +236,13 @@ export function tomlContentLoader(contentDir: string): Loader {
const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, ""); const id = relPath.replace(/\/index\.md$/, "").replace(/\.md$/, "");
currentHeadings = []; currentHeadings = [];
const trimmedBody = await renderCallouts(body.trim()); const trimmedBody = await renderCallouts(body.trim());
const html = await marked.parse(trimmedBody); const parsedHtml = await marked.parse(trimmedBody);
const { html, youtubeIds } = enhanceYoutubeEmbeds(parsedHtml);
const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length; const wordCount = trimmedBody.split(/\s+/).filter(Boolean).length;
const readingMinutes = Math.max(1, Math.round(wordCount / 200)); const readingMinutes = Math.max(1, Math.round(wordCount / 200));
store.set({ store.set({
id, id,
data: { ...data, wordCount, readingMinutes }, data: { ...data, wordCount, readingMinutes, youtubeIds },
rendered: { html, metadata: { headings: currentHeadings } }, rendered: { html, metadata: { headings: currentHeadings } },
}); });
} }
+14
View File
@@ -0,0 +1,14 @@
// Split out from constellation-scene.ts so checking WebGL support doesn't
// require fetching/evaluating the three.js bundle it lives alongside —
// Constellation.astro checks this before dynamically importing that module.
export function supportsWebGL(): boolean {
try {
const canvas = document.createElement("canvas");
return !!(
window.WebGLRenderingContext &&
(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))
);
} catch {
return false;
}
}
+59
View File
@@ -0,0 +1,59 @@
import { getCollection } from "astro:content";
import type { APIContext } from "astro";
import { siteConfig } from "../site.config";
function escapeXml(value: string): string {
return value
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
export async function GET(context: APIContext) {
const site = (context.site ?? new URL(siteConfig.baseUrl)).href.replace(/\/$/, "");
const writings = (
await getCollection("writings", ({ data }) => !data.draft)
).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const updated = writings[0]?.data.updated ?? writings[0]?.data.date ?? new Date();
const entries = writings
.map((entry) => {
const url = `${site}/writings/${entry.id}/`;
const published = entry.data.date.toISOString();
const entryUpdated = (entry.data.updated ?? entry.data.date).toISOString();
const categories = (entry.data.taxonomies?.tags ?? [])
.map((tag) => `<category term="${escapeXml(tag)}" />`)
.join("");
return `
<entry>
<title>${escapeXml(entry.data.title)}</title>
<link href="${url}" />
<id>${url}</id>
<published>${published}</published>
<updated>${entryUpdated}</updated>
${entry.data.description ? `<summary>${escapeXml(entry.data.description)}</summary>` : ""}
<author><name>${escapeXml(entry.data.author ?? siteConfig.author)}</name></author>
${categories}
</entry>`;
})
.join("");
const xml = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>${escapeXml(siteConfig.title)}</title>
<subtitle>${escapeXml(siteConfig.description)}</subtitle>
<link href="${site}/atom.xml" rel="self" />
<link href="${site}/" />
<id>${site}/</id>
<updated>${updated.toISOString()}</updated>
${entries}
</feed>`;
return new Response(xml, {
headers: { "Content-Type": "application/atom+xml; charset=utf-8" },
});
}
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
import "katex/dist/katex.min.css";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import Base from "../../layouts/Base.astro"; import Base from "../../layouts/Base.astro";
@@ -16,9 +17,9 @@ const rendered = await Promise.all(
{ {
rendered.map(({ entry, Content }) => ( rendered.map(({ entry, Content }) => (
<div class="card"> <div class="card">
<h3> <h2 class="entry-title">
<a href={entry.data.url}>{entry.data.title}</a> <a href={entry.data.url}>{entry.data.title}</a>
</h3> </h2>
<Content /> <Content />
</div> </div>
)) ))
@@ -1,4 +1,5 @@
--- ---
import "katex/dist/katex.min.css";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import BookSpines from "../../components/BookSpines.astro"; import BookSpines from "../../components/BookSpines.astro";
import Base from "../../layouts/Base.astro"; import Base from "../../layouts/Base.astro";
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
import "katex/dist/katex.min.css";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import BookSpines from "../../components/BookSpines.astro"; import BookSpines from "../../components/BookSpines.astro";
import Base from "../../layouts/Base.astro"; import Base from "../../layouts/Base.astro";
@@ -37,9 +38,9 @@ const withBodies = await Promise.all(
)} )}
</a> </a>
<div> <div>
<h3> <h2 class="entry-title">
<a href={`/bookshelf/${entry.id}/`}>{entry.data.title}</a> <span class="tag">{entry.data.kind}</span> <a href={`/bookshelf/${entry.id}/`}>{entry.data.title}</a> <span class="tag">{entry.data.kind}</span>
</h3> </h2>
<p class="meta"> <p class="meta">
{entry.data.authors.join(", ")} {entry.data.authors.join(", ")}
{entry.data.date_finished && <> · Read {entry.data.date_finished.toDateString()}</>} {entry.data.date_finished && <> · Read {entry.data.date_finished.toDateString()}</>}
+3 -3
View File
@@ -21,7 +21,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
<div class="columns"> <div class="columns">
<section> <section>
<p class="eyebrow">Writings</p> <h2 class="eyebrow">Writings</h2>
{ {
writings.length === 0 ? ( writings.length === 0 ? (
<p class="meta">Nothing published yet.</p> <p class="meta">Nothing published yet.</p>
@@ -40,7 +40,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
</section> </section>
<section> <section>
<p class="eyebrow">Portfolio</p> <h2 class="eyebrow">Portfolio</h2>
{ {
portfolio.length === 0 ? ( portfolio.length === 0 ? (
<p class="meta">Nothing published yet.</p> <p class="meta">Nothing published yet.</p>
@@ -57,7 +57,7 @@ const bookshelf = (await getCollection("bookshelf")).slice(0, 3);
</section> </section>
<section> <section>
<p class="eyebrow">Bookshelf</p> <h2 class="eyebrow">Bookshelf</h2>
{ {
bookshelf.length === 0 ? ( bookshelf.length === 0 ? (
<p class="meta">Nothing on the shelf yet.</p> <p class="meta">Nothing on the shelf yet.</p>
+1 -1
View File
@@ -15,7 +15,7 @@ const projects = (
{ {
projects.map((entry) => ( projects.map((entry) => (
<div class="card"> <div class="card">
<h3>{entry.data.title}</h3> <h2 class="entry-title">{entry.data.title}</h2>
<p class="meta">{entry.data.role}</p> <p class="meta">{entry.data.role}</p>
{entry.data.description && <p>{entry.data.description}</p>} {entry.data.description && <p>{entry.data.description}</p>}
{entry.data.outcome && <p>{entry.data.outcome}</p>} {entry.data.outcome && <p>{entry.data.outcome}</p>}
-22
View File
@@ -1,22 +0,0 @@
---
import Base from "../layouts/Base.astro";
---
<Base title="Search">
<h1>Search</h1>
<p class="meta">
Search only works against a built site (`pnpm build` runs Pagefind after
`astro build`) — it has no index to query in `astro dev`.
</p>
<div id="search"></div>
<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script is:inline src="/pagefind/pagefind-ui.js"></script>
<script is:inline>
window.addEventListener("DOMContentLoaded", () => {
if (window.PagefindUI) {
// biome-ignore lint/correctness/noUndeclaredVariables: loaded from /pagefind/pagefind-ui.js above
new PagefindUI({ element: "#search", showSubResults: true });
}
});
</script>
</Base>
+2 -2
View File
@@ -25,9 +25,9 @@ const description = siteConfig.tagDescriptions[tag];
{ {
writings.map((entry) => ( writings.map((entry) => (
<div class="card"> <div class="card">
<h3> <h2 class="entry-title">
<a href={`/writings/${entry.id}/`}>{entry.data.title}</a> <a href={`/writings/${entry.id}/`}>{entry.data.title}</a>
</h3> </h2>
<p class="meta">{entry.data.date.toDateString()}</p> <p class="meta">{entry.data.date.toDateString()}</p>
</div> </div>
)) ))
+52 -11
View File
@@ -9,18 +9,59 @@ for (const w of writings) {
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1); tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
} }
} }
const tags = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]); const tags = [...tagCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]));
--- ---
<Base title="All Tags"> <Base title="All Tags">
<h1>All Tags</h1> <div class="tags-page">
<ul> <h1>All Tags</h1>
{ <div class="tags-cloud">
tags.map(([tag, count]) => ( {
<li> tags.map(([tag, count]) => (
<a href={`/tags/${tag}/`}>{tag}</a> ({count}) <a href={`/tags/${tag}/`} class="tag-link">
</li> {tag}
)) <sup class="tag-count">{count}</sup>
} </a>
</ul> ))
}
</div>
</div>
</Base> </Base>
<style>
.tags-page {
text-align: center;
}
.tags-page h1 {
display: block;
}
.tags-cloud {
display: flex;
flex-wrap: wrap;
gap: var(--space-5);
justify-content: center;
align-items: flex-start;
margin-top: var(--space-6);
}
.tag-link {
font-size: var(--font-size-lg);
color: var(--text);
text-decoration: none;
display: inline-flex;
align-items: flex-start;
transition: color 0.2s var(--ease-out);
}
.tag-link:hover {
color: var(--accent);
}
.tag-count {
font-size: 0.65em;
color: var(--text-muted);
margin-left: 0.2rem;
}
</style>
+76 -30
View File
@@ -1,7 +1,13 @@
--- ---
import "katex/dist/katex.min.css";
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import TocList from "../../components/TocList.astro"; import TocList from "../../components/TocList.astro";
import Minimap from "../../components/Minimap.astro";
import ReadingProgress from "../../components/ReadingProgress.astro";
import ShareButtons from "../../components/ShareButtons.astro";
import Comments from "../../components/Comments.astro";
import Base from "../../layouts/Base.astro"; import Base from "../../layouts/Base.astro";
import { siteConfig } from "../../site.config";
export async function getStaticPaths() { export async function getStaticPaths() {
const writings = await getCollection("writings", ({ data }) => !data.draft); const writings = await getCollection("writings", ({ data }) => !data.draft);
@@ -33,26 +39,61 @@ const seriesEntries = series
: []; : [];
const tags = entry.data.taxonomies?.tags ?? []; const tags = entry.data.taxonomies?.tags ?? [];
const related = tags.length const shareUrl = new URL(Astro.url.pathname, siteConfig.baseUrl).href;
? allWritings const canonicalUrl = entry.data.canonical_url ?? shareUrl;
.filter((w) => w.id !== entry.id) const ogImageUrl = new URL(`/images/og/writings/${entry.id}.png`, siteConfig.baseUrl).href;
.map((w) => ({
entry: w,
overlap: (w.data.taxonomies?.tags ?? []).filter((t) => tags.includes(t))
.length,
}))
.filter((w) => w.overlap > 0)
.sort((a, b) => b.overlap - a.overlap)
.slice(0, 3)
.map((w) => w.entry)
: [];
const description = const description =
entry.data.extra?.llms_description ?? entry.data.description; entry.data.extra?.llms_description ?? entry.data.description;
const tocHeadings = headings.filter((h) => h.depth <= 3); const tocHeadings = headings.filter((h) => h.depth <= 3);
const articleJsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: entry.data.title,
description,
url: canonicalUrl,
image: ogImageUrl,
datePublished: entry.data.date.toISOString(),
dateModified: (entry.data.updated ?? entry.data.date).toISOString(),
keywords: tags.length ? tags.join(", ") : undefined,
author: { "@id": `${siteConfig.baseUrl}/#person` },
publisher: { "@id": `${siteConfig.baseUrl}/#person` },
mainEntityOfPage: canonicalUrl,
};
const breadcrumbJsonLd = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{ "@type": "ListItem", position: 1, name: "Home", item: siteConfig.baseUrl },
{ "@type": "ListItem", position: 2, name: "Writings", item: `${siteConfig.baseUrl}/writings/` },
{ "@type": "ListItem", position: 3, name: entry.data.title, item: canonicalUrl },
],
};
const videoJsonLd = entry.data.youtubeIds.map((id) => ({
"@context": "https://schema.org",
"@type": "VideoObject",
name: entry.data.title,
description,
thumbnailUrl: `https://i.ytimg.com/vi/${id}/sddefault.jpg`,
uploadDate: entry.data.date.toISOString(),
embedUrl: `https://www.youtube-nocookie.com/embed/${id}`,
contentUrl: `https://www.youtube.com/watch?v=${id}`,
}));
--- ---
<Base title={entry.data.title} description={description} wide ogImage={`/images/og/writings/${entry.id}.png`}> <Base
title={entry.data.title}
description={description}
wide
ogImage={`/images/og/writings/${entry.id}.png`}
ogType="article"
canonicalUrl={canonicalUrl}
structuredData={[articleJsonLd, breadcrumbJsonLd, ...videoJsonLd]}
>
<ReadingProgress />
<div class="article-layout"> <div class="article-layout">
<div class="article-main"> <div class="article-main">
<h1>{entry.data.title}</h1> <h1>{entry.data.title}</h1>
@@ -92,25 +133,28 @@ const tocHeadings = headings.filter((h) => h.depth <= 3);
<Content /> <Content />
</article> </article>
<nav class="post-nav" aria-label="Post navigation"> <ShareButtons title={entry.data.title} url={shareUrl} />
{prev && <a href={`/writings/${prev.id}/`}>← {prev.data.title}</a>}
{next && <a href={`/writings/${next.id}/`}>{next.data.title} →</a>}
</nav>
{ {
related.length > 0 && ( (prev || next) && (
<section> <nav class="post-nav" aria-label="Post navigation">
<h2>Related Posts</h2> {prev && (
{related.map((w) => ( <a class="post-nav-link post-nav-prev" href={`/writings/${prev.id}/`}>
<div class="card"> <span class="post-nav-label">← Previous</span>
<h3> <span class="post-nav-title">{prev.data.title}</span>
<a href={`/writings/${w.id}/`}>{w.data.title}</a> </a>
</h3> )}
</div> {next && (
))} <a class="post-nav-link post-nav-next" href={`/writings/${next.id}/`}>
</section> <span class="post-nav-label">Next →</span>
<span class="post-nav-title">{next.data.title}</span>
</a>
)}
</nav>
) )
} }
<Comments />
</div> </div>
{ {
@@ -121,13 +165,15 @@ const tocHeadings = headings.filter((h) => h.depth <= 3);
) )
} }
</div> </div>
<Minimap />
</Base> </Base>
<style> <style>
.article-layout { .article-layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 42rem); grid-template-columns: minmax(0, 42rem);
justify-content: center; justify-content: start;
align-items: start; align-items: start;
gap: var(--space-7); gap: var(--space-7);
} }
+1 -1
View File
@@ -12,7 +12,7 @@ const writings = (
{ {
writings.map((entry) => ( writings.map((entry) => (
<div class="card"> <div class="card">
<h3><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h3> <h2 class="entry-title"><a href={`/writings/${entry.id}/`}>{entry.data.title}</a></h2>
<p class="meta"> <p class="meta">
{entry.data.date.toDateString()} {entry.data.date.toDateString()}
{entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read</>} {entry.data.readingMinutes && <> · {entry.data.readingMinutes} min read</>}
+157 -23
View File
@@ -46,25 +46,16 @@ h3 {
line-height: 1.15; line-height: 1.15;
letter-spacing: -0.01em; letter-spacing: -0.01em;
margin: 0 0 var(--space-4); margin: 0 0 var(--space-4);
text-decoration-line: underline;
text-decoration-color: var(--accent);
text-decoration-thickness: 0.1em;
text-underline-offset: 0.08em;
text-decoration-skip-ink: auto;
} }
h1 { h1 {
font-size: var(--font-size-3xl); font-size: var(--font-size-3xl);
display: inline-block; display: inline-block;
position: relative;
}
h1::after {
content: "";
position: absolute;
left: 0.05em;
right: 0.4em;
bottom: 0.08em;
height: 0.09em;
background: var(--accent);
opacity: 0.55;
z-index: -1;
border-radius: 1px;
} }
h2 { h2 {
@@ -75,6 +66,22 @@ h3 {
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
} }
/* A plain <a> has its own default underline (in its own text color, not
the heading's accent), which paints over the heading's underline and
made link-wrapped titles inconsistent with plain-text ones (and with
each other, depending on visited/hover state). Forcing the decoration to
inherit the heading's actual values fixes that everywhere a title is
wrapped in a link, without needing a class on every call site. */
h1 a,
h2 a,
h3 a {
text-decoration-line: inherit;
text-decoration-color: inherit;
text-decoration-thickness: inherit;
text-underline-offset: inherit;
text-decoration-skip-ink: inherit;
}
p { p {
margin: 0 0 var(--space-5); margin: 0 0 var(--space-5);
} }
@@ -93,6 +100,22 @@ pre {
border: 1px solid var(--border); border: 1px solid var(--border);
} }
.youtube-wrapper {
position: relative;
aspect-ratio: 16 / 9;
margin: 0 0 var(--space-5);
border-radius: var(--radius-md);
overflow: hidden;
}
.youtube-wrapper iframe {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border: 0;
}
/* highlight.js syntax highlighting (toml-content-loader.ts renders code /* highlight.js syntax highlighting (toml-content-loader.ts renders code
fences through hljs's core build). Colors are drawn from the existing fences through hljs's core build). Colors are drawn from the existing
design tokens, so they already switch with the site's light/dark theme design tokens, so they already switch with the site's light/dark theme
@@ -112,16 +135,47 @@ pre {
color: var(--accent); color: var(--accent);
} }
/* #4a9c6d / #d1a53d have plenty of contrast on the dark theme's code-block
background but fail WCAG AA on the light one (2.8:1 / 1.9:1) — same issue
as the callout colors above, fixed the same way: darker variants for
light mode, originals restored for dark mode. */
.hljs-string, .hljs-string,
.hljs-attr, .hljs-attr,
.hljs-symbol, .hljs-symbol,
.hljs-addition { .hljs-addition {
color: #4a9c6d; color: #377451;
} }
.hljs-number, .hljs-number,
.hljs-meta, .hljs-meta,
.hljs-doctag { .hljs-doctag {
color: #82641f;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .hljs-string,
:root:not([data-theme="light"]) .hljs-attr,
:root:not([data-theme="light"]) .hljs-symbol,
:root:not([data-theme="light"]) .hljs-addition {
color: #4a9c6d;
}
:root:not([data-theme="light"]) .hljs-number,
:root:not([data-theme="light"]) .hljs-meta,
:root:not([data-theme="light"]) .hljs-doctag {
color: #d1a53d;
}
}
:root[data-theme="dark"] .hljs-string,
:root[data-theme="dark"] .hljs-attr,
:root[data-theme="dark"] .hljs-symbol,
:root[data-theme="dark"] .hljs-addition {
color: #4a9c6d;
}
:root[data-theme="dark"] .hljs-number,
:root[data-theme="dark"] .hljs-meta,
:root[data-theme="dark"] .hljs-doctag {
color: #d1a53d; color: #d1a53d;
} }
@@ -134,7 +188,7 @@ pre {
} }
.hljs-deletion { .hljs-deletion {
color: #d1502f; color: var(--accent);
} }
.hljs-emphasis { .hljs-emphasis {
@@ -192,9 +246,13 @@ pre {
.eyebrow { .eyebrow {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-style: normal;
font-weight: 400;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none;
color: var(--text-muted); color: var(--text-muted);
margin: 0 0 var(--space-5);
} }
.meta { .meta {
@@ -221,15 +279,24 @@ pre {
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
.card h3 { /* Semantically these list-item titles are h2 (the page's only other
heading is its own h1, with nothing at h2 between them) — sized down to
what used to be the h3 scale so going from h3 to h2 doesn't change the
visual hierarchy, just the accessibility tree. */
.entry-title {
margin-bottom: var(--space-1); margin-bottom: var(--space-1);
font-size: var(--font-size-xl);
} }
.card h3 a { .entry-title a {
text-decoration: none; text-decoration-line: underline;
text-decoration-color: var(--accent);
text-decoration-thickness: 0.1em;
text-underline-offset: 0.08em;
text-decoration-skip-ink: auto;
} }
.card h3 a:hover { .entry-title a:hover {
color: var(--accent); color: var(--accent);
} }
@@ -298,16 +365,83 @@ pre {
--callout-color: var(--accent); --callout-color: var(--accent);
} }
/* Dark-mode-only values: #4a9c6d and #d1a53d have plenty of contrast on the
dark --bg-elevated (5.4:1 / 7.9:1) but fail WCAG AA on the light one
(2.8:1 / 1.9:1), so light mode gets darkened variants instead of one
hardcoded color for both themes. */
.callout-tip { .callout-tip {
--callout-color: #4a9c6d; --callout-color: #377451;
} }
.callout-warning { .callout-warning {
--callout-color: #d1a53d; --callout-color: #82641f;
} }
.callout-danger { .callout-danger {
--callout-color: #d1502f; --callout-color: var(--accent);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .callout-tip {
--callout-color: #4a9c6d;
}
:root:not([data-theme="light"]) .callout-warning {
--callout-color: #d1a53d;
}
}
:root[data-theme="dark"] .callout-tip {
--callout-color: #4a9c6d;
}
:root[data-theme="dark"] .callout-warning {
--callout-color: #d1a53d;
}
.post-nav {
display: flex;
flex-direction: column;
gap: var(--space-5);
margin-top: var(--space-7);
padding-top: var(--space-6);
border-top: 1px solid var(--border);
}
.post-nav-link {
display: flex;
flex-direction: column;
gap: var(--space-1);
text-decoration: none;
}
.post-nav-label {
font-family: var(--font-mono);
font-size: var(--font-size-sm);
color: var(--accent);
}
.post-nav-title {
font-family: var(--font-display);
font-style: italic;
color: var(--text);
transition: color 0.2s var(--ease-out);
}
.post-nav-link:hover .post-nav-title {
color: var(--accent);
}
@media (min-width: 40rem) {
.post-nav {
flex-direction: row;
justify-content: space-between;
gap: var(--space-6);
}
.post-nav-next {
align-items: flex-end;
text-align: right;
}
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
+5 -2
View File
@@ -10,8 +10,11 @@
--text: #1c1a15; --text: #1c1a15;
--text-muted: #6f6a5c; --text-muted: #6f6a5c;
--border: rgba(28, 26, 21, 0.12); --border: rgba(28, 26, 21, 0.12);
--accent: #d1502f; /* #d1502f measured 4.0:1 on --bg, just under WCAG AA's 4.5:1 for normal
--accent-muted: rgba(209, 80, 47, 0.12); text (used at small sizes in nav/tags/links) — darkened to 5.7:1 while
keeping the same hue. */
--accent: #a84025;
--accent-muted: rgba(168, 64, 37, 0.12);
--font-display: "Fraunces Variable", ui-serif, Georgia, serif; --font-display: "Fraunces Variable", ui-serif, Georgia, serif;
--font-body: "Newsreader Variable", ui-serif, Georgia, serif; --font-body: "Newsreader Variable", ui-serif, Georgia, serif;
-62
View File
@@ -1,62 +0,0 @@
#!/usr/bin/env sh
set -e
: ${ZOLA_REPO='https://github.com/getzola/zola'}
: ${ZOLA_INSTALL_DIR="${HOME}/.cargo/bin"}
# Helpers for ANSI color and logging.
name="$(basename $0)"; esc=''; reset="${esc}[0m"
red() { local string="${1}"; echo "${esc}[31m${string}${reset}"; }
yellow() { local string="${1}"; echo "${esc}[33m${string}${reset}"; }
green() { local string="${1}"; echo "${esc}[32m${string}${reset}"; }
blue() { local string="${1}"; echo "${esc}[34m${string}${reset}"; }
magenta() { local string="${1}"; echo "${esc}[35m${string}${reset}"; }
log() { local string="${1}"; echo "[$(blue $name)] ${string}"; }
prompt_for_update() {
local current="${1}"
local latest="${2}"
[ -n "${current}" ] \
&& installed="You're using Zola $(yellow $current)" \
|| installed="Zola is $(red 'not installed')"
log "${installed}, but $(magenta $latest) is available."
# Prompt for an update. Exit the entire script if declined.
read -p "$(log "Would you like to install Zola $(magenta $latest)? [y/N]: ")" answer
case $answer in
[Yy]*) log "Downloading Zola $(magenta $latest) from GitHub." ;;
[Nn]*|*) log $(red "Aborted!"); exit ;;
esac
}
install_zola() {
local version="${1}"
local download_url="${ZOLA_REPO}/releases/download/${version}/zola-${version}-x86_64-unknown-linux-gnu.tar.gz"
curl --progress-bar --location $download_url | tar --gunzip --extract --directory=$ZOLA_INSTALL_DIR --file -
}
update_zola() {
# Find the current Zola version (if any).
[ -n "$(which zola 2> /dev/null)" ] && current_version=$(zola --version | awk '{print "v"$2}')
# Find the latest available Zola version. Use --refs to avoid listing
# annotated commit objects that include the dereference syntax '^{}'.
# Find more background at https://stackoverflow.com/a/15472310.
local latest_version=$(git ls-remote --tags --refs $ZOLA_REPO | # Fetch remote tags.
sort -t '/' -k 3 -V | # Sort them by version.
tail -n 1 | # Take the latest one.
awk -F / '{print $3}') # Return only the tag.
if [ "${current_version}" != "${latest_version}" ]; then
prompt_for_update "${current_version}" "${latest_version}"
install_zola "${latest_version}"
fi
log "You're using Zola $(magenta $latest_version). $(green "Everything is up-to-date!")"
}
update_zola
-17
View File
@@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ZOLA_VERSION=0.23.1
echo "Downloading Zola ${ZOLA_VERSION}..."
curl -sLJO "https://github.com/getzola/zola/releases/download/v${ZOLA_VERSION}/zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz"
echo "Extracting Zola..."
tar -xf zola-v${ZOLA_VERSION}-x86_64-unknown-linux-gnu.tar.gz
echo "Building site with Zola..."
node scripts/generate-llms.mjs
./zola build
echo "Build complete! Generated files:"
ls -la public/
+12
View File
@@ -104,3 +104,15 @@ feed_url = "https://.../rss.xml"
Body is a one-to-two sentence blurb on why you follow it. No live feed Body is a one-to-two sentence blurb on why you follow it. No live feed
fetching — this list is hand-curated and static. fetching — this list is hand-curated and static.
## Markdown extras (all body content)
- **LaTeX**: `$inline$` and `$$block$$` math renders to static HTML/MathML
at build time (`marked-katex-extension` in `toml-content-loader.ts`) — no
client-side JS shipped for it.
- **Callouts**: `{% <note> %} ... {% </note> %}` (also `tip`/`warning`/
`danger`) renders as a styled callout box.
- **YouTube embeds**: paste a plain `<iframe src="https://www.youtube-
nocookie.com/embed/VIDEO_ID" ...></iframe>` — it's automatically wrapped
in a responsive 16:9 container, gets `loading="lazy"`, and emits
`VideoObject` structured data. No shortcode needed.
@@ -1,15 +0,0 @@
+++
title = "Why This Site Exists"
description = "A short note on what this new site is, and why it's more than a tech blog."
author = "andwati"
date = 2026-08-30
draft = false
[taxonomies]
tags = ["meta"]
[extra]
llms_description = "An introductory post explaining the purpose of this personal digital-legacy site."
+++
This is a placeholder post establishing the `writings` content schema. It will
be replaced once the design system and migration from the old Zola posts are
ready.
-136
View File
@@ -1,136 +0,0 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
*.log
# Sphinx documentation
docs/_build/
docs/build/
docs/source/_autosummary/
docs/source/reference/
docs/source/_build/
docs/rendering_times.csv
#i18n
docs/i18n/gettext/.doctrees
docs/i18n/**/*.mo
docs/i18n/translatable.po
docs/i18n/untranslatable.po
# PyBuilder
target/
# Jupyter Notebook
jupyter/
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# PyCharm
/.idea/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
*.pyc
*.bak
.DS_Store
.floo
.flooignore
.vscode
.vs
*.xml
*.iml
media
.eggs/
build/
dist/
/media_dir.txt
# ^TODO: Remove the need for this with a proper config file
# Ignore the built dependencies
third_party/*
View File
-579
View File
@@ -1,579 +0,0 @@
from manim import *
# ─────────────────────────────────────────────
# Shared palette
# ─────────────────────────────────────────────
BG = "#0d1117"
ACCENT = "#58a6ff"
GREEN = "#3fb950"
ORANGE = "#f0883e"
RED = "#f85149"
PURPLE = "#bc8cff"
GRAY = "#8b949e"
WHITE = "#e6edf3"
config.background_color = BG
# ─────────────────────────────────────────────
# Helper: section title card
# ─────────────────────────────────────────────
def title_card(scene: Scene, title: str, subtitle: str = ""):
bg = Rectangle(width=14, height=8, fill_color=BG, fill_opacity=1, stroke_width=0)
bar = Line(LEFT * 6, RIGHT * 6, color=ACCENT, stroke_width=3)
t = Text(title, font="JetBrains Mono", color=WHITE, font_size=52, weight=BOLD)
s = Text(subtitle, font="JetBrains Mono", color=GRAY, font_size=28) if subtitle else VMobject()
t.move_to(UP * 0.4)
s.next_to(t, DOWN, buff=0.4)
bar.next_to(t, UP, buff=0.55)
grp = VGroup(bg, bar, t, s)
scene.play(FadeIn(bar), Write(t), FadeIn(s), run_time=1.4)
scene.wait(1.5)
scene.play(FadeOut(grp), run_time=0.6)
# ─────────────────────────────────────────────
# 1. Intro
# ─────────────────────────────────────────────
class S01_Intro(Scene):
def construct(self):
# Animated headline
headline = Text("From C to Machine Code", font="JetBrains Mono",
color=ACCENT, font_size=56, weight=BOLD)
sub = Text("What Actually Runs", font="JetBrains Mono",
color=WHITE, font_size=34)
byline = Text("mockingspectre", font="JetBrains Mono",
color=GRAY, font_size=22)
headline.move_to(UP * 1.2)
sub.next_to(headline, DOWN, buff=0.5)
byline.next_to(sub, DOWN, buff=0.7)
accent_line = Line(LEFT * 3.5, RIGHT * 3.5, color=ACCENT, stroke_width=2)
accent_line.next_to(sub, DOWN, buff=0.3)
self.play(Write(headline), run_time=1.6)
self.play(FadeIn(sub, shift=UP * 0.3), run_time=0.9)
self.play(GrowFromCenter(accent_line), run_time=0.6)
self.play(FadeIn(byline), run_time=0.7)
self.wait(2.5)
# C → ASM → Binary flow diagram
c_box = self._box("C Source", GREEN, LEFT * 4.5)
asm_box = self._box("Assembly", ORANGE, ORIGIN)
bin_box = self._box("Machine\nCode", RED, RIGHT * 4.5)
arr1 = Arrow(c_box.get_right(), asm_box.get_left(), color=ACCENT, buff=0.15)
arr2 = Arrow(asm_box.get_right(), bin_box.get_left(), color=ACCENT, buff=0.15)
lbl1 = Text("GCC compiler", font="JetBrains Mono", color=GRAY, font_size=18).next_to(arr1, UP, buff=0.1)
lbl2 = Text("assemble", font="JetBrains Mono", color=GRAY, font_size=18).next_to(arr2, UP, buff=0.1)
self.play(FadeOut(headline, sub, accent_line, byline), run_time=0.5)
self.play(FadeIn(c_box), run_time=0.5)
self.play(GrowArrow(arr1), FadeIn(lbl1), run_time=0.7)
self.play(FadeIn(asm_box), run_time=0.5)
self.play(GrowArrow(arr2), FadeIn(lbl2), run_time=0.7)
self.play(FadeIn(bin_box), run_time=0.5)
self.wait(2.5)
self.play(*[FadeOut(m) for m in self.mobjects])
@staticmethod
def _box(label, color, pos):
rect = RoundedRectangle(corner_radius=0.2, width=2.6, height=1.1,
fill_color=color, fill_opacity=0.15,
stroke_color=color, stroke_width=2)
txt = Text(label, font="JetBrains Mono", color=color, font_size=24)
grp = VGroup(rect, txt).move_to(pos)
return grp
# ─────────────────────────────────────────────
# 2. Hello World: C → Assembly walkthrough
# ─────────────────────────────────────────────
class S02_HelloWorld(Scene):
def construct(self):
title_card(self, "Hello World", "C → Assembly")
# Left: C code
c_src = [
("#include <stdio.h>", GRAY),
("", WHITE),
("int main(){", WHITE),
(' printf("hello world\\n");', GREEN),
(" return 0;", WHITE),
("}", WHITE),
]
c_lines = VGroup(*[
Text(line, font="JetBrains Mono", color=col, font_size=20)
for line, col in c_src
])
c_lines.arrange(DOWN, aligned_edge=LEFT, buff=0.18)
c_bg = BackgroundRectangle(c_lines, color="#161b22", fill_opacity=1,
buff=0.25, corner_radius=0.15)
c_code = VGroup(c_bg, c_lines).shift(LEFT * 3.4 + UP * 0.5)
c_label = Text("hello.c", font="JetBrains Mono", color=GREEN, font_size=22
).next_to(c_code, UP, buff=0.2)
self.play(FadeIn(c_label), FadeIn(c_code), run_time=1.0)
self.wait(0.8)
# Command
cmd_txt = Text("gcc -o hello hello.c && objdump -d -M intel hello",
font="JetBrains Mono", color=ORANGE, font_size=17
).shift(DOWN * 2.8)
self.play(Write(cmd_txt), run_time=1.2)
self.wait(0.5)
# Right: ASM output
asm_lines = [
("1139:", "55", "push rbp"),
("113a:", "48 89 e5", "mov rbp, rsp"),
("113d:", "48 8d 05 c0 0e", "lea rax, [rip+0xec0]"),
("1144:", "48 89 c7", "mov rdi, rax"),
("1147:", "e8 e4 fe ff ff", "call puts@plt"),
("114c:", "b8 00 00 00 00", "mov eax, 0x0"),
("1151:", "5d", "pop rbp"),
("1152:", "c3", "ret"),
]
asm_group = self._asm_block(asm_lines).shift(RIGHT * 3.1 + UP * 0.5)
asm_label = Text("objdump output", font="JetBrains Mono",
color=ORANGE, font_size=22).next_to(asm_group, UP, buff=0.2)
self.play(FadeIn(asm_label), FadeIn(asm_group), run_time=1.0)
self.wait(0.5)
# Annotate key lines — asm_group[0]=bg, asm_group[1]=VGroup of rows
rows = asm_group[1]
note = Text("rdi ← pointer to \"hello world\"",
font="JetBrains Mono", color=ACCENT, font_size=19
).next_to(asm_group, DOWN, buff=0.25)
self.play(rows[3].animate.set_color(ACCENT),
rows[4].animate.set_color(PURPLE),
Write(note), run_time=1.0)
self.wait(2.5)
self.play(*[FadeOut(m) for m in self.mobjects])
@staticmethod
def _asm_block(lines):
rows = VGroup()
for addr, hex_b, mnemonic in lines:
row = Text(f"{addr:<8} {hex_b:<20} {mnemonic}",
font="JetBrains Mono", color=WHITE, font_size=17)
rows.add(row)
rows.arrange(DOWN, aligned_edge=LEFT, buff=0.18)
bg = BackgroundRectangle(rows, color="#161b22", fill_opacity=1,
buff=0.2, corner_radius=0.15)
return VGroup(bg, rows)
# ─────────────────────────────────────────────
# 3. Registers
# ─────────────────────────────────────────────
class S03_Registers(Scene):
def construct(self):
title_card(self, "Registers", "The CPU's Workbench")
# Register diagram: rax broken into sub-registers
regs = [
("rax (64-bit / 8 bytes)", ACCENT, 4.8),
("eax (32-bit / 4 bytes)", GREEN, 3.2),
("ax (16-bit / 2 bytes)", ORANGE, 1.9),
("al ( 8-bit / 1 byte )", RED, 0.95),
]
bars = VGroup()
labels = VGroup()
for name, color, width in regs:
bar = Rectangle(width=width, height=0.52,
fill_color=color, fill_opacity=0.25,
stroke_color=color, stroke_width=2)
lbl = Text(name, font="JetBrains Mono", color=color, font_size=19)
lbl.next_to(bar, RIGHT, buff=0.25)
bars.add(bar)
labels.add(lbl)
for i, bar in enumerate(bars):
bar.align_to(bars[0], LEFT)
group = VGroup(*[VGroup(bars[i], labels[i]) for i in range(len(bars))])
group.arrange(DOWN, aligned_edge=LEFT, buff=0.22).shift(LEFT * 1 + UP * 0.5)
self.play(LaggedStart(*[FadeIn(g) for g in group], lag_ratio=0.3), run_time=1.6)
self.wait(1.0)
caption = Text("Smaller aliases point into the same physical register",
font="JetBrains Mono", color=GRAY, font_size=21
).shift(DOWN * 2.6)
self.play(Write(caption))
self.wait(1.5)
self.play(FadeOut(group, caption))
# Calling convention diagram
title_card(self, "Calling Convention", "Argument passing in x64 Linux")
args = [
("1st arg", "rdi", ACCENT),
("2nd arg", "rsi", GREEN),
("3rd arg", "rdx", ORANGE),
("4th arg", "rcx", PURPLE),
("5th arg", "r8", RED),
("6th arg", "r9", GRAY),
]
cols = VGroup()
for label, reg, color in args:
arg_t = Text(label, font="JetBrains Mono", color=WHITE, font_size=22)
arr = Arrow(LEFT * 0.1, RIGHT * 0.9, color=color, buff=0)
reg_t = Text(reg, font="JetBrains Mono", color=color,
font_size=26, weight=BOLD)
row = VGroup(arg_t, arr, reg_t).arrange(RIGHT, buff=0.3)
cols.add(row)
cols.arrange(DOWN, buff=0.28).shift(UP * 0.3)
ret_row = Text("return value → rax", font="JetBrains Mono",
color=ACCENT, font_size=24).next_to(cols, DOWN, buff=0.5)
self.play(LaggedStart(*[FadeIn(r) for r in cols], lag_ratio=0.22), run_time=1.8)
self.play(Write(ret_row))
self.wait(2.5)
self.play(*[FadeOut(m) for m in self.mobjects])
# ─────────────────────────────────────────────
# 4. The Stack
# ─────────────────────────────────────────────
class S04_Stack(Scene):
def construct(self):
title_card(self, "The Stack", "LIFO — Last In, First Out")
# Animated stack push/pop
stack_slots = VGroup()
slot_labels = []
N = 6
for i in range(N):
rect = Rectangle(width=3.2, height=0.72,
fill_color="#161b22", fill_opacity=1,
stroke_color=GRAY, stroke_width=1.5)
stack_slots.add(rect)
stack_slots.arrange(DOWN, buff=0).shift(LEFT * 3 + UP * 0.5)
self.play(FadeIn(stack_slots))
# Labels: lower address at top
addr_labels = VGroup()
for i, slot in enumerate(stack_slots):
a = Text(f"0x...{(0x28 - i*8):02x}", font="JetBrains Mono",
color=GRAY, font_size=16)
a.next_to(slot, RIGHT, buff=0.15)
addr_labels.add(a)
self.play(FadeIn(addr_labels))
# rsp / rbp pointers
rbp_ptr = Arrow(RIGHT * 0.8, stack_slots[-1].get_right() + LEFT * 0.01,
color=ORANGE, buff=0.05)
rbp_lbl = Text("rbp", font="JetBrains Mono", color=ORANGE,
font_size=22, weight=BOLD).next_to(rbp_ptr, RIGHT, buff=0.1)
rsp_ptr = Arrow(RIGHT * 0.8, stack_slots[-1].get_right() + LEFT * 0.01,
color=ACCENT, buff=0.05)
rsp_lbl = Text("rsp", font="JetBrains Mono", color=ACCENT,
font_size=22, weight=BOLD).next_to(rsp_ptr, RIGHT, buff=0.1)
self.play(FadeIn(rbp_ptr, rbp_lbl))
# Push simulation: fill slots from bottom up
push_values = ["saved rbp", "local: x=5", "saved rip", "arg 1", "arg 2"]
colors_ = [ORANGE, GREEN, RED, ACCENT, PURPLE]
current_top = N - 1
for val, col in zip(push_values, colors_):
slot = stack_slots[current_top]
fill = Rectangle(width=3.2, height=0.72,
fill_color=col, fill_opacity=0.22,
stroke_color=col, stroke_width=1.5)
fill.move_to(slot)
txt = Text(val, font="JetBrains Mono", color=col, font_size=18)
txt.move_to(slot)
rsp_ptr.put_start_and_end_on(
RIGHT * 0.8, stack_slots[current_top].get_right() + LEFT * 0.01)
rsp_lbl.next_to(rsp_ptr, RIGHT, buff=0.1)
if current_top == N - 1:
self.play(FadeIn(rsp_ptr, rsp_lbl))
self.play(FadeIn(fill), Write(txt), run_time=0.55)
current_top -= 1
if current_top >= 0:
new_end = stack_slots[current_top].get_right() + LEFT * 0.01
self.play(rsp_ptr.animate.put_start_and_end_on(RIGHT * 0.8, new_end),
rsp_lbl.animate.next_to(rsp_ptr, RIGHT, buff=0.1),
run_time=0.4)
caption = Text("Stack grows toward lower addresses",
font="JetBrains Mono", color=GRAY, font_size=20
).shift(DOWN * 3.1)
self.play(Write(caption))
self.wait(2.5)
# Show the C code + ASM side by side
self.play(*[FadeOut(m) for m in self.mobjects])
title_card(self, "Stack in Assembly", "sub rsp allocates space")
asm_lines = [
("1139:", "55", "push rbp"),
("113a:", "48 89 e5", "mov rbp, rsp"),
("113d:", "48 83 ec 10", "sub rsp, 0x10 ; allocate locals"),
("1141:", "c7 45 fc 05…", "mov DWORD PTR [rbp-0x4], 0x5"),
("1152:", "e8 d9 fe ff ff", "call puts@plt"),
("1157:", "b8 00 00 00 00", "mov eax, 0x0"),
("115c:", "c9", "leave"),
("115d:", "c3", "ret"),
]
asm_group = S02_HelloWorld._asm_block(asm_lines).scale(0.95).shift(UP * 0.3)
self.play(FadeIn(asm_group))
# Highlight sub rsp
rows = asm_group.submobjects[1]
self.play(rows[2].animate.set_color(ACCENT), run_time=0.5)
note = Text("Reserves 16 bytes on the stack for local variable x",
font="JetBrains Mono", color=ACCENT, font_size=20
).shift(DOWN * 2.8)
self.play(Write(note))
self.wait(2)
self.play(*[FadeOut(m) for m in self.mobjects])
# ─────────────────────────────────────────────
# 5. The Heap
# ─────────────────────────────────────────────
class S05_Heap(Scene):
def construct(self):
title_card(self, "The Heap", "Dynamic Memory & malloc/free")
# Memory layout diagram
segments = [
("Stack ↓", ORANGE, 1.2),
(" ... ", BG, 0.5),
("Heap ↑", GREEN, 1.2),
(".bss", PURPLE, 0.55),
(".data", PURPLE, 0.55),
(".text", ACCENT, 0.55),
]
layout = VGroup()
for label, color, h in segments:
rect = Rectangle(width=3.8, height=h,
fill_color=color, fill_opacity=0.18,
stroke_color=color if color != BG else GRAY,
stroke_width=1.5)
lbl = Text(label, font="JetBrains Mono", color=color if color != BG else GRAY,
font_size=22)
lbl.move_to(rect)
layout.add(VGroup(rect, lbl))
layout.arrange(DOWN, buff=0).shift(LEFT * 3.5)
low_lbl = Text("Low Address", font="JetBrains Mono", color=GRAY, font_size=17
).next_to(layout, DOWN, buff=0.2)
high_lbl = Text("High Address", font="JetBrains Mono", color=GRAY, font_size=17
).next_to(layout, UP, buff=0.2)
self.play(FadeIn(layout), Write(low_lbl), Write(high_lbl), run_time=1.2)
self.wait(0.6)
# malloc/free sequence on the right
steps = [
("char *buf = malloc(64);", GREEN, "allocate 64 bytes on heap"),
("strcpy(buf, \"hello\");", ORANGE, "write data into buffer"),
("free(buf);", RED, "return memory to allocator"),
]
step_group = VGroup()
for code, color, desc in steps:
code_t = Text(code, font="JetBrains Mono", color=color, font_size=19)
desc_t = Text(desc, font="JetBrains Mono", color=GRAY, font_size=16)
desc_t.next_to(code_t, DOWN, buff=0.12)
step_group.add(VGroup(code_t, desc_t))
step_group.arrange(DOWN, buff=0.5).shift(RIGHT * 2.2 + UP * 0.3)
for step in step_group:
self.play(FadeIn(step, shift=RIGHT * 0.3), run_time=0.7)
self.wait(0.6)
# Vulnerability callout
vuln_box = RoundedRectangle(corner_radius=0.2, width=6.5, height=1.8,
fill_color=RED, fill_opacity=0.1,
stroke_color=RED, stroke_width=2)
vuln_box.shift(DOWN * 2.8)
vuln_txt = Text(
"Forget free → Memory Leak\n"
"free + reuse → Use-After-Free\n"
"Overflow 64 bytes → Heap Overflow",
font="JetBrains Mono", color=RED, font_size=17, line_spacing=1.3)
vuln_txt.move_to(vuln_box)
self.play(FadeIn(vuln_box), Write(vuln_txt), run_time=1.2)
self.wait(2.5)
self.play(*[FadeOut(m) for m in self.mobjects])
# ─────────────────────────────────────────────
# 6. Instruction Set
# ─────────────────────────────────────────────
class S06_Instructions(Scene):
def construct(self):
title_card(self, "CPU Instructions", "mov · lea · push · call · ret")
categories = [
("Data Movement", [
("mov rax, rdx", "copy rdx into rax"),
("mov rax, [rdx]", "dereference: load value at addr rdx"),
("lea rdi, [rbx+0x10]","load address rbx+0x10 into rdi"),
], ACCENT),
("Arithmetic / Logic", [
("add rax, 0x4", "rax = rax + 4"),
("sub rsp, 0x10", "grow stack by 16 bytes"),
("xor rax, rax", "zero rax (optimised)"),
], GREEN),
("Stack", [
("push rbp", "decrement rsp; write rbp to [rsp]"),
("pop rbp", "read [rsp] into rbp; increment rsp"),
], ORANGE),
("Control Flow", [
("call 0x401234", "push rip; jump to function"),
("ret", "pop saved rip; resume caller"),
("jmp 0x401234", "unconditional jump"),
("jz / jnz", "jump if Zero Flag set / not set"),
], PURPLE),
]
for cat_name, instructions, color in categories:
self.play(*[FadeOut(m) for m in self.mobjects], run_time=0.3)
hdr = Text(cat_name, font="JetBrains Mono", color=color,
font_size=34, weight=BOLD).shift(UP * 3)
self.play(Write(hdr))
rows = VGroup()
for instr, desc in instructions:
instr_t = Text(instr, font="JetBrains Mono", color=color, font_size=24)
desc_t = Text(f" → {desc}", font="JetBrains Mono",
color=WHITE, font_size=20)
row = VGroup(instr_t, desc_t).arrange(RIGHT, buff=0.3)
rows.add(row)
rows.arrange(DOWN, aligned_edge=LEFT, buff=0.4).shift(UP * 0.4)
self.play(LaggedStart(*[FadeIn(r, shift=LEFT * 0.3) for r in rows],
lag_ratio=0.25), run_time=1.2)
self.wait(2.0)
self.play(*[FadeOut(m) for m in self.mobjects])
# ─────────────────────────────────────────────
# 7. CPU Flags
# ─────────────────────────────────────────────
class S07_Flags(Scene):
def construct(self):
title_card(self, "CPU Flags", "EFLAGS register")
# 16-bit flag register visual
bits = list(range(15, -1, -1))
important = {0: ("CF", RED), 2: ("PF", GRAY), 6: ("ZF", ACCENT),
7: ("SF", ORANGE), 11: ("OF", PURPLE)}
cells = VGroup()
for b in bits:
color = important.get(b, ("", GRAY))[1] if b in important else GRAY
rect = Square(side_length=0.55,
fill_color=color, fill_opacity=0.18,
stroke_color=color, stroke_width=1.5)
bit_n = Text(str(b), font="JetBrains Mono",
color=color, font_size=13).move_to(rect)
cells.add(VGroup(rect, bit_n))
cells.arrange(RIGHT, buff=0.04).shift(UP * 1.8)
self.play(FadeIn(cells), run_time=1.0)
# Flag abbreviations below important bits
for b, (abbr, color) in important.items():
idx = 15 - b
cell = cells[idx]
lbl = Text(abbr, font="JetBrains Mono", color=color, font_size=18,
weight=BOLD)
lbl.next_to(cell, DOWN, buff=0.15)
self.play(FadeIn(lbl), run_time=0.3)
self.wait(0.5)
# cmp / jz example
example = VGroup(
Text("cmp rax, 0x5 ; sets ZF=1 if rax == 5",
font="JetBrains Mono", color=WHITE, font_size=21),
Text("jz target ; jumps only if ZF=1",
font="JetBrains Mono", color=ACCENT, font_size=21),
).arrange(DOWN, aligned_edge=LEFT, buff=0.3).shift(DOWN * 0.4)
bg = BackgroundRectangle(example, color="#161b22", fill_opacity=1,
buff=0.25, corner_radius=0.15)
self.play(FadeIn(bg), FadeIn(example), run_time=1.0)
# Animate ZF cell lighting up
zf_idx = 15 - 6
zf_cell_rect = cells[zf_idx][0]
self.play(zf_cell_rect.animate.set_fill(ACCENT, opacity=0.75), run_time=0.7)
self.wait(0.5)
self.play(zf_cell_rect.animate.set_fill(ACCENT, opacity=0.18), run_time=0.4)
self.wait(2.0)
self.play(*[FadeOut(m) for m in self.mobjects])
# ─────────────────────────────────────────────
# 8. Outro
# ─────────────────────────────────────────────
class S08_Outro(Scene):
def construct(self):
recap_items = [
"C compiles to assembly, which becomes machine code",
"Registers: rip, rsp, rbp, argument registers",
"Calling convention: rdi → rsi → rdx → rcx → r8 → r9",
"Stack: LIFO, managed by rsp/rbp",
"Heap: dynamic memory via malloc/free",
"Instructions: mov, lea, push/pop, call/ret, jz/jnz",
"Flags register drives conditional branching",
]
title = Text("Recap", font="JetBrains Mono", color=ACCENT,
font_size=44, weight=BOLD).shift(UP * 3.1)
self.play(Write(title))
items_grp = VGroup()
for txt in recap_items:
bullet = Text(f"• {txt}", font="JetBrains Mono",
color=WHITE, font_size=20)
items_grp.add(bullet)
items_grp.arrange(DOWN, aligned_edge=LEFT, buff=0.28).shift(UP * 0.5)
self.play(LaggedStart(*[FadeIn(i, shift=LEFT * 0.2) for i in items_grp],
lag_ratio=0.18), run_time=2.5)
self.wait(1.5)
next_lbl = Text("Next: ELF Binary Structure",
font="JetBrains Mono", color=ORANGE, font_size=28,
weight=BOLD).shift(DOWN * 2.8)
self.play(Write(next_lbl))
self.wait(1.0)
support = Text("buymeacoffee.com/mockingspectre",
font="JetBrains Mono", color=GRAY, font_size=18
).shift(DOWN * 3.5)
self.play(FadeIn(support))
self.wait(3.0)
self.play(FadeOut(*self.mobjects))
# ─────────────────────────────────────────────
# Full video: concatenate all scenes
# ─────────────────────────────────────────────
class FullVideo(S01_Intro, S02_HelloWorld, S03_Registers,
S04_Stack, S05_Heap, S06_Instructions,
S07_Flags, S08_Outro):
"""Render all scenes back-to-back as a single video file."""
def construct(self):
S01_Intro.construct(self)
S02_HelloWorld.construct(self)
S03_Registers.construct(self)
S04_Stack.construct(self)
S05_Heap.construct(self)
S06_Instructions.construct(self)
S07_Flags.construct(self)
S08_Outro.construct(self)
-9
View File
@@ -1,9 +0,0 @@
[project]
name = "manimations"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"manim>=0.20.1",
]
-718
View File
@@ -1,718 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "audioop-lts"
version = "0.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" },
{ url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" },
{ url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" },
{ url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" },
{ url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" },
{ url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" },
{ url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" },
{ url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" },
{ url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" },
{ url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" },
{ url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" },
{ url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" },
{ url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" },
{ url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" },
{ url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" },
{ url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" },
{ url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" },
{ url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" },
{ url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" },
{ url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" },
{ url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" },
{ url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" },
{ url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" },
{ url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" },
{ url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" },
{ url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" },
{ url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" },
{ url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" },
{ url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" },
{ url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" },
{ url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" },
{ url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" },
{ url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" },
{ url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" },
{ url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" },
{ url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" },
{ url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" },
{ url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" },
]
[[package]]
name = "av"
version = "17.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/eb/abca886df3a091bc406feb5ff71b4c4f426beaae6b71b9697264ce8c7211/av-17.0.0.tar.gz", hash = "sha256:c53685df73775a8763c375c7b2d62a6cb149d992a26a4b098204da42ade8c3df", size = 4410769, upload-time = "2026-03-14T14:38:45.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/fb/55e3b5b5d1fc61466292f26fbcbabafa2642f378dc48875f8f554591e1a4/av-17.0.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ed4013fac77c309a4a68141dcf6148f1821bb1073a36d4289379762a6372f711", size = 23238424, upload-time = "2026-03-14T14:38:05.856Z" },
{ url = "https://files.pythonhosted.org/packages/52/03/9ace1acc08bc9ae38c14bf3a4b1360e995e4d999d1d33c2cbd7c9e77582a/av-17.0.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:e44b6c83e9f3be9f79ee87d0b77a27cea9a9cd67bd630362c86b7e56a748dfbb", size = 18709043, upload-time = "2026-03-14T14:38:08.288Z" },
{ url = "https://files.pythonhosted.org/packages/00/c0/637721f3cd5bb8bd16105a1a08efd781fc12f449931bdb3a4d0cfd63fa55/av-17.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b440da6ac47da0629d509316f24bcd858f33158dbdd0f1b7293d71e99beb26de", size = 34018780, upload-time = "2026-03-14T14:38:10.45Z" },
{ url = "https://files.pythonhosted.org/packages/d2/59/d19bc3257dd985d55337d7f0414c019414b97e16cd3690ebf9941a847543/av-17.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1060cba85f97f4a337311169d92c0b5e143452cfa5ca0e65fa499d7955e8592e", size = 36358757, upload-time = "2026-03-14T14:38:13.092Z" },
{ url = "https://files.pythonhosted.org/packages/52/6c/a1f4f2677bae6f2ade7a8a18e90ebdcf70690c9b1c4e40e118aa30fa313f/av-17.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:deda202e6021cfc7ba3e816897760ec5431309d59a4da1f75df3c0e9413d71e7", size = 35195281, upload-time = "2026-03-14T14:38:15.789Z" },
{ url = "https://files.pythonhosted.org/packages/90/ea/52b0fc6f69432c7bf3f5fbe6f707113650aa40a1a05b9096ffc2bba4f77d/av-17.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffaf266a1a9c2148072de0a4b5ae98061465178d2cfaa69ee089761149342974", size = 37444817, upload-time = "2026-03-14T14:38:18.563Z" },
{ url = "https://files.pythonhosted.org/packages/34/ad/d2172966282cb8f146c13b6be7416efefde74186460c5e1708ddfc13dba6/av-17.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:45a35a40b2875bf2f98de7c952d74d960f92f319734e6d28e03b4c62a49e6f49", size = 28888553, upload-time = "2026-03-14T14:38:21.223Z" },
{ url = "https://files.pythonhosted.org/packages/b0/bb/c5a4c4172c514d631fb506e6366b503576b8c7f29809cf42aca73e28ff01/av-17.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:3d32e9b5c5bbcb872a0b6917b352a1db8a42142237826c9b49a36d5dbd9e9c26", size = 21916910, upload-time = "2026-03-14T14:38:23.706Z" },
{ url = "https://files.pythonhosted.org/packages/7f/8e/c40ac08e63f79387c59f6ecc38f47d4c942b549130eee579ec1a91f6a291/av-17.0.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:d13250fb4b4522e9a6bec32da082556d5f257110ea223758151375748d9bbe25", size = 23483029, upload-time = "2026-03-14T14:38:25.758Z" },
{ url = "https://files.pythonhosted.org/packages/a9/fb/b4419494bfc249163ec393c613966d66db7e95c76da3345711cd115a79df/av-17.0.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:dbb56aa3b7ae72451d1bf6e9d37c7d83d39b97af712f73583ff419fbf08fc237", size = 18920446, upload-time = "2026-03-14T14:38:27.905Z" },
{ url = "https://files.pythonhosted.org/packages/30/62/c2306d91602ddad2c56106f21dcb334fd51d5ea2e952f7fa025bb8aa39fc/av-17.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a213ac9e83b7ab12c2e9f277a09cac8e9d85cf0883efdab7a87a60e2e4e48879", size = 37477266, upload-time = "2026-03-14T14:38:30.404Z" },
{ url = "https://files.pythonhosted.org/packages/28/cd/c8510a9607886785c0b3ca019d503e888c3757529be42a7287fe2bfa92d5/av-17.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:e15c88bb0921f9435bcc5a27a0863dba571a80ad5e1389c4fcf2073833bb4a74", size = 39572988, upload-time = "2026-03-14T14:38:32.984Z" },
{ url = "https://files.pythonhosted.org/packages/7d/2d/207d9361e25b5abec9be335bbab4df6b6b838e2214be4b374f4cfb285427/av-17.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:096cfd1e9fc896506726c7c42aaf9b370e78c2f257cde4d6ddb6c889bfcc49ec", size = 38399591, upload-time = "2026-03-14T14:38:35.465Z" },
{ url = "https://files.pythonhosted.org/packages/73/ca/307740c6aa2980966bf11383ffcb04bacc5b13f3d268ab4cfb274ad6f793/av-17.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3649ab3d2c7f58049ded1a36e100c0d8fd529cf258f41dd88678ba824034d8c9", size = 40590681, upload-time = "2026-03-14T14:38:38.269Z" },
{ url = "https://files.pythonhosted.org/packages/35/f2/6fdb26d0651adf409864cb2a0d60da107e467d3d1aabc94b234ead54324a/av-17.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e5002271ab2135b551d980c2db8f3299d452e3b9d3633f24f6bb57fffe91cd10", size = 29216337, upload-time = "2026-03-14T14:38:40.83Z" },
{ url = "https://files.pythonhosted.org/packages/41/0a/0896b829a39b5669a2d811e1a79598de661693685cd62b31f11d0c18e65b/av-17.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dba98603fc4665b4f750de86fbaf6c0cfaece970671a9b529e0e3d1711e8367e", size = 22071058, upload-time = "2026-03-14T14:38:43.663Z" },
]
[[package]]
name = "beautifulsoup4"
version = "4.14.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "soupsieve" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
]
[[package]]
name = "click"
version = "8.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
]
[[package]]
name = "cloup"
version = "3.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/64/7f0a66021ff81d51859c66adc13f3c71f0306c2f8dfb9877a0694cbada05/cloup-3.0.9.tar.gz", hash = "sha256:519f524d3c64040e49a0866b5fc0bfd6af3eac0d3d6a4b2b50b33ab0247db2d7", size = 229896, upload-time = "2026-04-04T03:53:54.182Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/15/435bb5a24e3a2dc8c26afedab0c6f0cf4291c66fb99509c168c38eb81135/cloup-3.0.9-py2.py3-none-any.whl", hash = "sha256:c761ef4e975454335d50c0e1eb83595a932735aac06e8955b3ced774bc62ae7b", size = 54724, upload-time = "2026-04-04T03:53:52.909Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cython"
version = "3.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" },
{ url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" },
{ url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
]
[[package]]
name = "decorator"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]]
name = "glcontext"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/80/8238a0e6e972292061176141c1028b5e670aa8c94cf4c2f819bd730d314e/glcontext-3.0.0.tar.gz", hash = "sha256:57168edcd38df2fc0d70c318edf6f7e59091fba1cd3dadb289d0aa50449211ef", size = 16422, upload-time = "2024-08-10T20:01:20.004Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/be/0ef6a6710164fde818040238b041b02a082a2b9d210f18632ab2354d863e/glcontext-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5bd37089570d3cdb01c6c0b315c49ce8a4dcdab2c431f5ba9f37a8b633cebfdf", size = 9366, upload-time = "2024-08-10T20:00:30.531Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d7/c3220898d72fbf938660ba5789c19cf245d21b45802a5d86cbcc67d66413/glcontext-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:857fd83e60f15580afd369dfb651a10d84a70ec35995622d253551bfb3ff9477", size = 9735, upload-time = "2024-08-10T20:00:31.708Z" },
{ url = "https://files.pythonhosted.org/packages/74/c1/2d57062d2f2f6e55c58b12bdeab2a39b209f959df32039d099ecdfe96bf7/glcontext-3.0.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93fda9b378ce6d91f366e83e71ebdafdd167280a9834d1d6341ce6457c4e42ed", size = 50166, upload-time = "2024-08-10T20:00:32.627Z" },
{ url = "https://files.pythonhosted.org/packages/03/4d/7fee00c76d678b06529e939ab6f3e3190af30208fe2e984526899026a437/glcontext-3.0.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89ad50d34aa62f03f6aaf6ae39fc27afd1b0eaefb0281aac51f686dc5672d473", size = 51186, upload-time = "2024-08-10T20:00:33.718Z" },
{ url = "https://files.pythonhosted.org/packages/f0/58/dc9a56192b889587e51ea511804bad5dec816de81ce16831db9fe19d5c40/glcontext-3.0.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2634d5e9647a6d7b0c5a5c0c57e91ac98aa79759bffb42459af4374b049fab01", size = 44658, upload-time = "2024-08-10T20:00:35.232Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ed/acb12e67589deaa96ad29d6994c2b9383afd18700f4f2a42ff342628aac5/glcontext-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0140c5df37cb48271527355062d35589dc3e1e7e73b51adf9962ed5048115f69", size = 46882, upload-time = "2024-08-10T20:00:36.33Z" },
{ url = "https://files.pythonhosted.org/packages/e0/6d/033ba23466d596c945f5f31f5c4e50cba3bc6664fdee58f6ceab54b76f4b/glcontext-3.0.0-cp313-cp313-win32.whl", hash = "sha256:6678e0552b516fa8fe62f500ef2b953bec991e82a003be2a9840d16556d03d2e", size = 12220, upload-time = "2024-08-10T20:00:37.874Z" },
{ url = "https://files.pythonhosted.org/packages/53/b4/f0e0860526b8661ec6ae2b25a15b61100e551f57f488613c564752173a56/glcontext-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:18aa4b1df50e8c8ea39bd0f775f39bcc987521f92c4ed019ec7d70078471354d", size = 12971, upload-time = "2024-08-10T20:00:39.233Z" },
]
[[package]]
name = "isosurfaces"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/cf/bd7e70bb7b8dfd77afdc79aba8d83afd4a9263f045861cd4ddd34b7f6a12/isosurfaces-0.1.2.tar.gz", hash = "sha256:fa51ebe864ea9355b26830e27fdd6a41d5a58b419fa8d4b47e3b8b80718d6e21", size = 11348, upload-time = "2024-02-26T00:20:52.066Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/68/d5e9e6e0d6e43107d8393d2ee3d231dbb597bf93052c6f3117b313724980/isosurfaces-0.1.2-py3-none-any.whl", hash = "sha256:525a49ba93f4dbc35303cd2faf30976af0f99d9274cfa2787aec016b8ef96c64", size = 11649, upload-time = "2024-02-26T00:20:41.308Z" },
]
[[package]]
name = "manim"
version = "0.20.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "audioop-lts" },
{ name = "av" },
{ name = "beautifulsoup4" },
{ name = "click" },
{ name = "cloup" },
{ name = "decorator" },
{ name = "isosurfaces" },
{ name = "manimpango" },
{ name = "mapbox-earcut" },
{ name = "moderngl" },
{ name = "moderngl-window" },
{ name = "networkx" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "pycairo" },
{ name = "pydub" },
{ name = "pygments" },
{ name = "rich" },
{ name = "scipy" },
{ name = "screeninfo" },
{ name = "skia-pathops" },
{ name = "srt" },
{ name = "svgelements" },
{ name = "tqdm" },
{ name = "typing-extensions" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/3b/ad54ce02f3e95d17d016cb1254708ae3795b60d5661f3b2085655940a565/manim-0.20.1.tar.gz", hash = "sha256:1e9747fb2fc1bde58ad09bcbd77d141793ce4b61811726a7fce537193d92e16b", size = 553537, upload-time = "2026-02-27T07:32:53.54Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/74/7a54f81bbb3016036662c5497edc89c5d053a8ab1e8f7c8faaca5d43b174/manim-0.20.1-py3-none-any.whl", hash = "sha256:2c76784716aef88630eace2762645e1eb25f236fe750cb1b3c4fdd3558de49e1", size = 652109, upload-time = "2026-02-27T07:32:55.066Z" },
]
[[package]]
name = "manimations"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "manim" },
]
[package.metadata]
requires-dist = [{ name = "manim", specifier = ">=0.20.1" }]
[[package]]
name = "manimpango"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/55/d360e73eb4d04b102cef399ddebcba486a9b6c1977a26fe710beffd52e95/manimpango-0.6.1.tar.gz", hash = "sha256:59a00bbf8e99dab5f94341087c88e609fe946e79724627429cf59da84cbd40bf", size = 4080834, upload-time = "2025-10-23T06:04:48.564Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/ee/01cd6b3087a5ead9483add3023f46c135bddd7773d506b771b44bbe0830b/manimpango-0.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:79aafe6373cb31dcdd1ee505407ec2bea18e74aed877f93dcd27ec9d88584472", size = 7751685, upload-time = "2025-10-23T06:04:28.457Z" },
{ url = "https://files.pythonhosted.org/packages/70/93/905ac20a9190655870131940dadb5121645cf8a23e6ea0f04be7f4604f2e/manimpango-0.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1049bda27ca504d24b48bec21756f4d8a3310ea7bc690167c54d05e97b8f7639", size = 7062629, upload-time = "2025-10-23T06:04:30.059Z" },
{ url = "https://files.pythonhosted.org/packages/76/88/b2de7d2d3a0331cf79992f8c6da0f0abd873d4ec9f024cad5992f1cf7a6d/manimpango-0.6.1-cp313-cp313-win32.whl", hash = "sha256:8da8238147b96737b36725b0fc386dc1dd7901516dfd7e73b2e7d34cd5d934f1", size = 3626705, upload-time = "2025-10-23T06:04:31.527Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ed/a7a57491b26e8fb85ac0be9c39b53a6690f3c39beaf06d3715cb6916932b/manimpango-0.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:e90d6b926d0e673ce624963d44cc28c55a368b6074108ab885a43305baeae26c", size = 4203874, upload-time = "2025-10-23T06:04:32.981Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c0/318410215b409465a237c126a3e1f612c432d33b037b2f997870df327434/manimpango-0.6.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:318d14087901ee27ed6d0cfef0335aed57db217f8de831c8ca4e4c52b7805b45", size = 7751895, upload-time = "2025-10-23T06:04:34.874Z" },
{ url = "https://files.pythonhosted.org/packages/d1/83/f0a05e3e6312ca9673bc8b470c6a743cf2d0bd7f08b218efc993226e72e4/manimpango-0.6.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ffe2f8353857f0ca9a16894430d871051da450bc2d19beb969cf8b57b032f580", size = 7063387, upload-time = "2025-10-23T06:04:36.877Z" },
{ url = "https://files.pythonhosted.org/packages/13/76/0748323341647e8c338bd7af6fb527ffdc43d676003b57743740bdfcffcc/manimpango-0.6.1-cp314-cp314-win32.whl", hash = "sha256:c294ab801c8ffc217342dfeff7aeca97cbea86cd8b4e6e8c9a8d7da020b820e1", size = 3737933, upload-time = "2025-10-23T06:04:38.775Z" },
{ url = "https://files.pythonhosted.org/packages/2b/e9/42b995549cfd137780045e44b3bfba2cb1247b18bd2fb9f97079048e78ac/manimpango-0.6.1-cp314-cp314-win_amd64.whl", hash = "sha256:d8b0ee675eb30bbb67097cecb2be469847c3ccf574291f2bd43360caeb5f62ed", size = 4357908, upload-time = "2025-10-23T06:04:40.641Z" },
]
[[package]]
name = "mapbox-earcut"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/7b/bbf6b00488662be5d2eb7a188222c264b6f713bac10dc4a77bf37a4cb4b6/mapbox_earcut-2.0.0.tar.gz", hash = "sha256:81eab6b86cf99551deb698b98e3f7502c57900e5c479df15e1bdaf1a57f0f9d6", size = 39934, upload-time = "2025-11-16T18:41:27.251Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/7c/c5dd5b255b9828ba5df729e62fdd470a322c938f07ef392ca03c0592bb3a/mapbox_earcut-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:582329a81bd36cf0f82e443c395bcb8cfdb10caddafec76acaebac7c20bf1c31", size = 55619, upload-time = "2025-11-16T18:40:44.44Z" },
{ url = "https://files.pythonhosted.org/packages/1a/3f/03f23eac9831e7d0d8da3d6993695a9a3724659c94e9997f6b7aaccc199d/mapbox_earcut-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d2ac5f610b3e44a3a0c4df06b5552d503b4f1c2c409eeca20dbe05112bd60955", size = 52023, upload-time = "2025-11-16T18:40:45.857Z" },
{ url = "https://files.pythonhosted.org/packages/39/f3/a92ccee494b3e437e4bd81ecd358e39d231dc90af010d6c43930506c10ad/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58cc88513b87734b243d86f0d3fb87e96e0a78d9abd8fd615c55f766dd63f949", size = 56357, upload-time = "2025-11-16T18:40:47.27Z" },
{ url = "https://files.pythonhosted.org/packages/03/30/e54ececd0403a5495c340b693075abec92a6d17dc44283b6cb059534f7ed/mapbox_earcut-2.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40218d887798451932f3c335992834aa807c35cd497c6e0733470fdbd77f9521", size = 59215, upload-time = "2025-11-16T18:40:48.682Z" },
{ url = "https://files.pythonhosted.org/packages/6e/e1/8fbff13a074c1fbf702b30ce7ec4d878bc664d659c1c2b1697831f4ea3a8/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:39fa5cfa0e855b028ec9b0200c88ebfa252448f343ce2f67b6fc07fe1f22a3ae", size = 152304, upload-time = "2025-11-16T18:40:49.85Z" },
{ url = "https://files.pythonhosted.org/packages/b9/d5/c757030b3cb3a9f2278ded6f7312d2b9d3761db6f3da8d395f7f7303dd66/mapbox_earcut-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:476b558473b8a43f238d46e819bc0f830c427842ec5feb19e23b4dcac8ad2455", size = 157270, upload-time = "2025-11-16T18:40:51.093Z" },
{ url = "https://files.pythonhosted.org/packages/96/63/589c6decb1f032d8811f1066da552f0a718830f592e6d6539fa4c3c766b8/mapbox_earcut-2.0.0-cp313-cp313-win32.whl", hash = "sha256:8c2d125c182acbc490b39503c0dec4f937bae180d0849a26bcea0ee4a76024bd", size = 51207, upload-time = "2025-11-16T18:40:52.285Z" },
{ url = "https://files.pythonhosted.org/packages/76/75/a79a6020c46d4f07731e88ec5cc9324f6b43343aba835def1dc0bf59fecf/mapbox_earcut-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e049e6a37c228d7a9cb2f54ae405aa21d35c5175d849530fb32064ddb38ad5ab", size = 56416, upload-time = "2025-11-16T18:40:53.474Z" },
{ url = "https://files.pythonhosted.org/packages/ce/5f/83e878c2b3e9e6db1f60b598a2cc5ed4c2b5bc8d281575c964869414a159/mapbox_earcut-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:8a833d73d63d4b6291bbd8b4d2f551e87f663282cdc547ecbbd9b423849ee996", size = 50103, upload-time = "2025-11-16T18:40:54.954Z" },
{ url = "https://files.pythonhosted.org/packages/96/fc/f1b74324c83f510213ff91eb8b1d2697ad5a12418c5fba966e80f1104a5f/mapbox_earcut-2.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad1dc141797037b7d4c9d8d2e52b9665b36294913a8ec31008b282d1a95b9bdc", size = 55728, upload-time = "2025-11-16T18:40:56.098Z" },
{ url = "https://files.pythonhosted.org/packages/7b/59/053c04e29c4bd22157d3b6255f1e5c19c46cb7a594c4314298bdcbca723f/mapbox_earcut-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0f0f5c6f5ed8ffdce8efe6a003ba598089d0ee07eabd41868db183be50484f9f", size = 52063, upload-time = "2025-11-16T18:40:57.227Z" },
{ url = "https://files.pythonhosted.org/packages/a6/77/acc2d553c3bb8c769535a280545bb7d9608141e90511a2e6215a54611776/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82cd92775f37fd1e4b8464c5e74a00e87130eecc55ee3df2492b8ca2bdf6ef3e", size = 56522, upload-time = "2025-11-16T18:40:58.349Z" },
{ url = "https://files.pythonhosted.org/packages/1a/f5/627dd6defd3c1a2b3069e9e27482aa04d268c841735e576c1e22848a34f6/mapbox_earcut-2.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:626ffc1310e0cc8910283e4ac3139e5fb0458f18f2c4874162f66159951933ff", size = 59204, upload-time = "2025-11-16T18:41:00.095Z" },
{ url = "https://files.pythonhosted.org/packages/4a/3e/819185542ab095ba1244ad65ececb3edcde6fd0111248a0f9318d695bfcf/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ea951d764a356cad95b23fef950d8aa3b44b933795ad09d977fea7d4dbe377c3", size = 152550, upload-time = "2025-11-16T18:41:01.233Z" },
{ url = "https://files.pythonhosted.org/packages/a9/ad/85e0f815e4774b90ad6761bce55c80d13ee21b2a24014b0be0d5010b0049/mapbox_earcut-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df1f217624abb5e02ecabcbd84369de970b8d8bc1e4e7c164c1cfcaddad76ca3", size = 157322, upload-time = "2025-11-16T18:41:02.866Z" },
{ url = "https://files.pythonhosted.org/packages/27/4c/0f56369e7a000d2f3177d17baf34263559b206ae524fcd0c4c5d1d960dab/mapbox_earcut-2.0.0-cp314-cp314-win32.whl", hash = "sha256:6fa61307d38b50fc9bd5449c00dbae46d270a32b372c6fc3b8af4b85c85746e4", size = 52916, upload-time = "2025-11-16T18:41:04.122Z" },
{ url = "https://files.pythonhosted.org/packages/c2/9d/8c557dd9b3d9fe2344f5bd5ff3bb0b2a42ed6addb7e43ca4358051743b04/mapbox_earcut-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:0da20ed3c81b240450118773bcedfac34e70a56998f66147222c46f4356fff67", size = 57713, upload-time = "2025-11-16T18:41:05.204Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ec/678c5553938d3a29d02dd41dd898672267f054afc4e2821958dee6ec86ce/mapbox_earcut-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:847e74bd5878e4c64793dc100f9288f5443f87c55c3fe391fd90509029136ff6", size = 51872, upload-time = "2025-11-16T18:41:06.323Z" },
{ url = "https://files.pythonhosted.org/packages/18/37/94f2d973669cbfef811e536713fe56ec012ba74e5f8795a832337b1866a3/mapbox_earcut-2.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ddc9e7175fc903185c64afbbf91febee56b50787dd0962fce2bfb4f20cf80d27", size = 56447, upload-time = "2025-11-16T18:41:07.443Z" },
{ url = "https://files.pythonhosted.org/packages/c9/1c/e0afcc82659cc1727a7e59c4f9e9880bbc3f048a4a5325772b44d4a91dfd/mapbox_earcut-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6dc8a7568066af9a858018d6d92b7e77e164578f9fcd79093f1cbe4ec203461b", size = 53154, upload-time = "2025-11-16T18:41:08.618Z" },
{ url = "https://files.pythonhosted.org/packages/6c/2d/9845281c8c35da2bea733b8c2df5b9fe694e73e7b05fe8a1d4c3c439a1bc/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6abc5340edd9b433ab2dab2ee033082a199d5c51cce445124626c0040ec0d81b", size = 56285, upload-time = "2025-11-16T18:41:09.728Z" },
{ url = "https://files.pythonhosted.org/packages/97/8e/eeea762a519490662b8f480e2b35bf03701b0bcc5a446b62a4c5a1500b06/mapbox_earcut-2.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df7afdd8078a9aa28f469d9242531d304e09a4b14e514f048e021a949f3777b4", size = 58601, upload-time = "2025-11-16T18:41:10.872Z" },
{ url = "https://files.pythonhosted.org/packages/b9/67/932f80aa6af9bc1a317b6119052c74f327d81e00b457003a049e324b810c/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a286f73e612a46cafd6d6c843365265090517af16823e2f37277c13cd8b6f09", size = 154924, upload-time = "2025-11-16T18:41:12.104Z" },
{ url = "https://files.pythonhosted.org/packages/87/38/5db4a91f9f90cbb447be61da5468a2955fad3a840ae4c7dbde789b09d45a/mapbox_earcut-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8d081fe1d00dc553e3e68c02fc395324aad0d8ed955f3ff59289264c9b21ace4", size = 159194, upload-time = "2025-11-16T18:41:13.364Z" },
{ url = "https://files.pythonhosted.org/packages/6b/03/de3843b13fe854a010fb2f8b25551d4d5fe1c879ff2e7c8d7d8d7d735a8e/mapbox_earcut-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:13049ca96431bbc7ef7fd7780dd1872209ca11a5c1977f7aa91a1b574a8af863", size = 54143, upload-time = "2025-11-16T18:41:14.564Z" },
{ url = "https://files.pythonhosted.org/packages/9a/89/fbdee5a56ba51df9be6098b5428636ad75aa994e98d8bec6113d5cba401e/mapbox_earcut-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ace78e4fdba3b8cbb7768d44d77a981698305862a07f94bbb6f5cc16659adb4", size = 60833, upload-time = "2025-11-16T18:41:15.694Z" },
]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "moderngl"
version = "5.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "glcontext" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/52/540e2f8c45060bb2709f56eb5a44ae828dfcc97ccecb342c1a7deb467889/moderngl-5.12.0.tar.gz", hash = "sha256:52936a98ccb2f2e1d6e3cb18528b2919f6831e7e3f924e788b5873badce5129b", size = 193232, upload-time = "2024-10-17T12:36:28.002Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/13/cf493bdc3cb4f7a6b4fb357e683404dc8a97d19f53d501e4afdd679538e2/moderngl-5.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f6efb432f5164f871471d1da36e3a4be9dc3efd7a1e48d0ac6b751e556af5d02", size = 112118, upload-time = "2024-10-17T12:37:14.892Z" },
{ url = "https://files.pythonhosted.org/packages/a9/1f/1d84bba5f42fb19ce240d08d5434fe8e2f34341e68bb9fa89336b6cbdcfc/moderngl-5.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9b09d8d15b2eaab41c8646a664429ec86af225fa25096758497cd212489d2e1e", size = 109371, upload-time = "2024-10-17T12:37:15.992Z" },
{ url = "https://files.pythonhosted.org/packages/be/ae/bda0b95878e2b36eac66f64d88c08e6c8ea759607f7d40e843a21c2f4f32/moderngl-5.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:071042dd4846e58cbe204cf49341b62cd209fdcb6d48018feb5a61c66707fcb2", size = 296206, upload-time = "2024-10-17T12:37:17.204Z" },
{ url = "https://files.pythonhosted.org/packages/28/bc/93dc73251bcdb9c0f6f5c9a1d97ec2134672c307c68e6106948eab1f73d4/moderngl-5.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91db8302ac7f5d7a82a967388677e1378ff078f1e16d05da37ce77f4633b93b1", size = 270443, upload-time = "2024-10-17T12:37:18.44Z" },
{ url = "https://files.pythonhosted.org/packages/2e/a1/bd72c788b16c2392d3e1ebb570e56d7f871eaa1854f57917c0f131acb365/moderngl-5.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:51971d65ec96a212a814350c8b324ae0754353e1b61826d1a06aa2d060df170e", size = 1345675, upload-time = "2024-10-17T12:37:20.077Z" },
{ url = "https://files.pythonhosted.org/packages/b2/ec/6aff8fa267d9f80e4d32b7a606fdf9f0563103441a7dbaa4f53e272a4ada/moderngl-5.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d56827360c19e831e986243b5daaf6a51006f1ec0d5372084ad446308763d19f", size = 1274906, upload-time = "2024-10-17T12:37:22.372Z" },
{ url = "https://files.pythonhosted.org/packages/9f/4d/dc3ff763c125080e71b1095875f5dcc80949402019abc073bdfdbed1f4c2/moderngl-5.12.0-cp313-cp313-win32.whl", hash = "sha256:caa432c12b138a6c9571719075c4d103bdc2504cd31aeda38a00ad10fcf268cb", size = 101215, upload-time = "2024-10-17T12:37:23.833Z" },
{ url = "https://files.pythonhosted.org/packages/2c/8b/0a264732e0ee49fca109e98ec28f4d0c326ffc31466aa6e9668e8961aabb/moderngl-5.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e34d1cd38f7998258f76a08bb5e87f351ec653b7ea1928b2711f8719c10cefd1", size = 108514, upload-time = "2024-10-17T12:37:25.304Z" },
]
[[package]]
name = "moderngl-window"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "moderngl" },
{ name = "numpy" },
{ name = "pillow" },
{ name = "pyglet" },
{ name = "pyglm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/90/02/92e235891300c901f59647112a0267a07454f58aeb2041aa44f6b85f9cb3/moderngl_window-3.1.1.tar.gz", hash = "sha256:29c2827505f87399f3461d480b2778910fddeebe44ea803301215cf212a6c1bc", size = 353495, upload-time = "2025-01-19T10:07:56.133Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/41/d3503f07de8ad3597165f88b07e1064629f82e65385c4307875b2f6b137f/moderngl_window-3.1.1-py3-none-any.whl", hash = "sha256:e3b3ac2b4e23afcbfdac1971318a4db893bed3ba6a8fbde3367b1226af39b2e5", size = 382381, upload-time = "2025-01-19T10:07:53.606Z" },
]
[[package]]
name = "networkx"
version = "3.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
]
[[package]]
name = "numpy"
version = "2.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
{ url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
{ url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
{ url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
{ url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
{ url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
{ url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
{ url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
{ url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
{ url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
{ url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
{ url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
{ url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
{ url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
{ url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
{ url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
{ url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
{ url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
{ url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
{ url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
{ url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
{ url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
{ url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
{ url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
{ url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
{ url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
{ url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
{ url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
{ url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
{ url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
{ url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
{ url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
{ url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
{ url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
]
[[package]]
name = "pillow"
version = "12.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
]
[[package]]
name = "pycairo"
version = "1.29.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/d9/1728840a22a4ef8a8f479b9156aa2943cd98c3907accd3849fb0d5f82bfd/pycairo-1.29.0.tar.gz", hash = "sha256:f3f7fde97325cae80224c09f12564ef58d0d0f655da0e3b040f5807bd5bd3142", size = 665871, upload-time = "2025-11-11T19:13:01.584Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/21/3f477dc318dd4e84a5ae6301e67284199d7e5a2384f3063714041086b65d/pycairo-1.29.0-cp313-cp313-win32.whl", hash = "sha256:3eb382a4141591807073274522f7aecab9e8fa2f14feafd11ac03a13a58141d7", size = 750949, upload-time = "2025-11-11T19:12:12.198Z" },
{ url = "https://files.pythonhosted.org/packages/43/34/7d27a333c558d6ac16dbc12a35061d389735e99e494ee4effa4ec6d99bed/pycairo-1.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:91114e4b3fbf4287c2b0788f83e1f566ce031bda49cf1c3c3c19c3e986e95c38", size = 844149, upload-time = "2025-11-11T19:12:19.171Z" },
{ url = "https://files.pythonhosted.org/packages/15/43/e782131e23df69e5c8e631a016ed84f94bbc4981bf6411079f57af730a23/pycairo-1.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:09b7f69a5ff6881e151354ea092137b97b0b1f0b2ab4eb81c92a02cc4a08e335", size = 693595, upload-time = "2025-11-11T19:12:23.445Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fa/87eaeeb9d53344c769839d7b2854db7ff2cd596211e00dd1b702eeb1838f/pycairo-1.29.0-cp314-cp314-win32.whl", hash = "sha256:69e2a7968a3fbb839736257bae153f547bca787113cc8d21e9e08ca4526e0b6b", size = 767198, upload-time = "2025-11-11T19:12:42.336Z" },
{ url = "https://files.pythonhosted.org/packages/3c/90/3564d0f64d0a00926ab863dc3c4a129b1065133128e96900772e1c4421f8/pycairo-1.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:e91243437a21cc4c67c401eff4433eadc45745275fa3ade1a0d877e50ffb90da", size = 871579, upload-time = "2025-11-11T19:12:48.982Z" },
{ url = "https://files.pythonhosted.org/packages/5e/91/93632b6ba12ad69c61991e3208bde88486fdfc152be8cfdd13444e9bc650/pycairo-1.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:b72200ea0e5f73ae4c788cd2028a750062221385eb0e6d8f1ecc714d0b4fdf82", size = 719537, upload-time = "2025-11-11T19:12:55.016Z" },
{ url = "https://files.pythonhosted.org/packages/93/23/37053c039f8d3b9b5017af9bc64d27b680c48a898d48b72e6d6583cf0155/pycairo-1.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5e45fce6185f553e79e4ef1722b8e98e6cde9900dbc48cb2637a9ccba86f627a", size = 874015, upload-time = "2025-11-11T19:12:28.47Z" },
{ url = "https://files.pythonhosted.org/packages/d7/54/123f6239685f5f3f2edc123f1e38d2eefacebee18cf3c532d2f4bd51d0ef/pycairo-1.29.0-cp314-cp314t-win_arm64.whl", hash = "sha256:caba0837a4b40d47c8dfb0f24cccc12c7831e3dd450837f2a356c75f21ce5a15", size = 721404, upload-time = "2025-11-11T19:12:36.919Z" },
]
[[package]]
name = "pydub"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
]
[[package]]
name = "pyglet"
version = "2.1.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/a6/ffd78b2d05a820ced85baa11210b8e5d0db0d951767021150be0a231df00/pyglet-2.1.14.tar.gz", hash = "sha256:feb3fba05a85f50f793688d88ddfbcdc57044d971f2d8d2107eb7578eb4af51e", size = 6595146, upload-time = "2026-04-05T08:24:35.549Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/d4/789d34a4674a4ccc0b65ad54d0f963f873d39000c3ebed0880574d5437e5/pyglet-2.1.14-py3-none-any.whl", hash = "sha256:70febf03f206607103e1f9806527019c589002626937aecdca4d095ce0a4b9b0", size = 1032928, upload-time = "2026-04-05T08:24:29.068Z" },
]
[[package]]
name = "pyglm"
version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/41/8b/bdaf7b9cacecd28f7b4c6fc2d7d136824c506ad38cfdb37a05ea7ec88694/pyglm-2.8.3.tar.gz", hash = "sha256:161781ea4d1267f796b645f85ebff53aeb8ee4f13b4e993c04d64c96d286e534", size = 584038, upload-time = "2025-11-26T12:12:59.47Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/22/ee11dff20adfc6aac3e0482ed275843e0b9854dbc2e812ab56cdacbed8d6/pyglm-2.8.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a6d07b6b73e55a36e1b6daa63acb53f4912f2ddf88f434af4cc70441435aef8", size = 1608547, upload-time = "2025-11-26T12:11:46.426Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5f/10c4cd636c3e6c63328a476f84bb03501d8016f12053c3a82ba588fc61ce/pyglm-2.8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eaf6241f7c3ff169575e11da78f4439422517e42558961332db4cd09e9599267", size = 1362651, upload-time = "2025-11-26T12:11:47.555Z" },
{ url = "https://files.pythonhosted.org/packages/ff/70/2c7fe768900ee9d0f87e7a89375fa7d83b5b0a8f0eee8d0ad06b22a96e37/pyglm-2.8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1ec0dfb8f2c848c4ee6330c70a1ca9333004776c8e5ec76096e0b67c739f688", size = 12157160, upload-time = "2025-11-26T12:11:49.042Z" },
{ url = "https://files.pythonhosted.org/packages/b0/a6/befefccf1c8a0a66f09a7a1a1d324a3f988cb5e6b634026280518e5a83cd/pyglm-2.8.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3eea9093210afb946769c84fe31f17a9f73696a1161ee84fb42e9255f8c5cc8a", size = 12787899, upload-time = "2025-11-26T12:11:50.946Z" },
{ url = "https://files.pythonhosted.org/packages/7c/d2/5475d0791b585ae26dc0d690862a0e1e6fda243552a1718628dd937e5543/pyglm-2.8.3-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:0c652650912dbd88994fa02ec9dba2f3b35dc3995427b6ae8056ae542d5a060a", size = 10886936, upload-time = "2025-11-26T12:11:53.628Z" },
{ url = "https://files.pythonhosted.org/packages/e1/a1/035068410f60ed53007e0488af96f7e4634b679e9156d1090e70732b2159/pyglm-2.8.3-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:e3bdda68f75ad270e66b0fc7a1883749489cca14c44cbe22bac038fa170a8a1c", size = 11711601, upload-time = "2025-11-26T12:11:55.367Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e6/286d1879eae87199f086136b08c70a3bff27880417f98dadc0cbcdb65e50/pyglm-2.8.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08f8b1bd0d80ce396ee9cd5d3d4c7aeb4bbfa2a54dc73413924e3c9982412528", size = 11939755, upload-time = "2025-11-26T12:11:57.23Z" },
{ url = "https://files.pythonhosted.org/packages/72/58/f40f109ac025bb18412db78e6e73ffc20a17b6b495de90352f5aabfb9982/pyglm-2.8.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12fa61feefa5a255097d0887415bce9dd72c1dd5e5a8c6577121ee5348336a16", size = 12774242, upload-time = "2025-11-26T12:11:59.051Z" },
{ url = "https://files.pythonhosted.org/packages/17/fd/71b44ee5ac341e9979731a7b868e237f3775e140a9481cea79bae7abb83c/pyglm-2.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:33118ef1d678ab573546757dee7f0a1ca2fba8e8d7760c9fe6320fe0cfa3deb7", size = 1663010, upload-time = "2025-11-26T12:12:00.914Z" },
{ url = "https://files.pythonhosted.org/packages/fa/1e/9b8ba9d4627585797d8bda952412c93a4c09b70cb25e64756a52accddc1e/pyglm-2.8.3-cp313-cp313-win_arm64.whl", hash = "sha256:73ff3785dfc4ce017626d7ab56d6711a7119c29e2e71294efed73810c1d307f9", size = 1239893, upload-time = "2025-11-26T12:12:02.032Z" },
{ url = "https://files.pythonhosted.org/packages/65/fe/494d7dce3fcaf0123e787320b4a558d8fe733d6d0e23b7fe51e687f88dc2/pyglm-2.8.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b312012458d0537b3d84f24f4ba51fd3930df7a0773fc643e36f8df27b807c7a", size = 1610593, upload-time = "2025-11-26T12:12:03.136Z" },
{ url = "https://files.pythonhosted.org/packages/27/c0/37cced4a1b29957a29baebc1f0034be5d5419adf12da4b99140c56152cdb/pyglm-2.8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3249bfe5352e18cc777fac591665679a99f270e1ff27cd11dc349af07684f007", size = 1366295, upload-time = "2025-11-26T12:12:04.176Z" },
{ url = "https://files.pythonhosted.org/packages/a7/ba/186176d1c3e26196ae4bec4b228bdbb1304576f2038a7f1635070923ee48/pyglm-2.8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb8ea0d6721c763a26eacde59b2c9165719050dcf49c99a2857f6e1e5a5f30bb", size = 12140715, upload-time = "2025-11-26T12:12:05.429Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3e/8d9f307649e9b79b34ae51303e46e7012cc2550d60e6dd00ea8d3d8c9cf2/pyglm-2.8.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:83effb89e2cf6dd79cf9ebecf2f9fbda3d25a92b61af264de3cadad408911de6", size = 12764551, upload-time = "2025-11-26T12:12:07.403Z" },
{ url = "https://files.pythonhosted.org/packages/4c/d6/2481433abe537d7019d9ce75eba86b57e1731e3e0352fd1ac7f31f4d1883/pyglm-2.8.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:e958d65ed55f2716fd8a3a2ef872cc52893ea7300d7feec62dccb27ec25fbc2f", size = 10869966, upload-time = "2025-11-26T12:12:09.198Z" },
{ url = "https://files.pythonhosted.org/packages/fe/bf/3c78a9718e1d26c5b6ec468a584eb30cd797c46b6edd08e79cb1d67e8bf0/pyglm-2.8.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:ec7cc14d2eb9f46a18012ee7c1a164e0395b058ceb6e341bf6d986316b698574", size = 11693025, upload-time = "2025-11-26T12:12:10.948Z" },
{ url = "https://files.pythonhosted.org/packages/d3/0d/00ef293153b6ca7e56d1be6facbd9d0a8e331d0c6d3114f47f2cf2913eda/pyglm-2.8.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4beb0ada21e7641a577f274496451befcde79965467ef4027bd933334b3de39b", size = 11920727, upload-time = "2025-11-26T12:12:12.845Z" },
{ url = "https://files.pythonhosted.org/packages/4e/10/d8ecf9b5ac3b6fc1b179b33dfbd087b8f4d4b12bab99b5794f569a15a99a/pyglm-2.8.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77878c1b8c713b9e39fe32f870d82e20c864da3b11a3b875eca06c270b04cdbe", size = 12759345, upload-time = "2025-11-26T12:12:14.705Z" },
{ url = "https://files.pythonhosted.org/packages/a1/d1/cc1f75ee77fd2f9bcb122bc7f3a628710fe3f0bc517333fa9f38268a7065/pyglm-2.8.3-cp314-cp314-win_amd64.whl", hash = "sha256:15c77bc46ff69d945565309e13ca99c4a001d6a941a80c45f26fbdec80fa16c4", size = 1710524, upload-time = "2025-11-26T12:12:16.486Z" },
{ url = "https://files.pythonhosted.org/packages/35/51/74c0a3107567dc769c06b6c04c7bc828f33792e089036c637334b7e4c573/pyglm-2.8.3-cp314-cp314-win_arm64.whl", hash = "sha256:2b16ec33bd43c514502bae8de2b319d168259090e101e1cde79cd0a7d33e1185", size = 1271521, upload-time = "2025-11-26T12:12:17.611Z" },
{ url = "https://files.pythonhosted.org/packages/32/8e/867045b54da8257b21c71c0255ba7a231c251aae4b5f26b12eb50b651cc7/pyglm-2.8.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6a1f1ab8debc06e0fdedb3f4285ded4bec38bf075652c393039838504767e6cf", size = 1633871, upload-time = "2025-11-26T12:12:18.791Z" },
{ url = "https://files.pythonhosted.org/packages/93/f5/81bb8b52e132dc1ecc87b7ffb50c714b4fd2f71f98b801dd376e036f942b/pyglm-2.8.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a7835d18747ab9f8e736e343cc35bee0a514f18add282f1fc8035945fcf9d9bd", size = 1387757, upload-time = "2025-11-26T12:12:20.497Z" },
{ url = "https://files.pythonhosted.org/packages/32/40/8581283c00e2a18a6bb20a9192e25a792b605c53dee1c3abb8871eb0d3ca/pyglm-2.8.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:af3ddee3d150bbef68ee7338ccd3e0710b75b08121b8efd52d786b0d2b6731be", size = 12724921, upload-time = "2025-11-26T12:12:21.802Z" },
{ url = "https://files.pythonhosted.org/packages/a0/0e/bc3c03038da822d1a66c38e166d4c89b6bdab846e576ac226442813af7f4/pyglm-2.8.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a76c8eaba0c58f5738e87be5efcd16c4e75540fe6ebfdf15c236a799a61358e", size = 13317946, upload-time = "2025-11-26T12:12:23.947Z" },
{ url = "https://files.pythonhosted.org/packages/3b/21/b1f52dc73d610e36aa3ccf3ef61634530e6f97c2ca809a057839bf578667/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:879ee9ab3c8ab47b1de59fe7e593eda854b8349f274ca60f057b83f3a405b84d", size = 11371582, upload-time = "2025-11-26T12:12:25.768Z" },
{ url = "https://files.pythonhosted.org/packages/16/aa/e03cc7a2daceb1bbdf98780a991a323e4078254584cf6664984a626ecfe1/pyglm-2.8.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:3e37c15b6c3e08f960b34ff9ec42e73469dfd868aec214e8a347da6c9d0245d6", size = 12192848, upload-time = "2025-11-26T12:12:28.606Z" },
{ url = "https://files.pythonhosted.org/packages/69/07/be893aee24a9a8b5400feda4d032fad9d281047d4379aa9f5ef4bea1f6a4/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd34a8670debef4a55bc756b14cbe8b0a4daa49f8f6850c86c5e11d20554927f", size = 12414214, upload-time = "2025-11-26T12:12:30.438Z" },
{ url = "https://files.pythonhosted.org/packages/0c/06/6bb4e8a09f7dd2bf8dc1f4cc5edc938fc1247a1b2770833515fb418fc695/pyglm-2.8.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:699f852e0335b79d0b664ba1c2d02cb4689256cda786e7780e821f60b0824c46", size = 13237516, upload-time = "2025-11-26T12:12:32.412Z" },
{ url = "https://files.pythonhosted.org/packages/2b/5c/7f15edd05020540748dad8f5eae0a07ed7f14f699bff530172b8850bd998/pyglm-2.8.3-cp314-cp314t-win_amd64.whl", hash = "sha256:78caadaf9cc2ddea1c55b0d44fa8032f35c9f821a6f152b72422e5657d38f01d", size = 1764614, upload-time = "2025-11-26T12:12:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f9/8bc8d010503a250319c55d2ceec5f90b0e807c7f85d581638c5ecd0a81de/pyglm-2.8.3-cp314-cp314t-win_arm64.whl", hash = "sha256:69400ad1852ca0972e4d9cbef9d9510941d4b81dc0fffebc5ac796a85440a119", size = 1292992, upload-time = "2025-11-26T12:12:35.681Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pyobjc-core"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/b6/d5612eb40be4fd5ef88c259339e6313f46ba67577a95d86c3470b951fce0/pyobjc_core-12.1.tar.gz", hash = "sha256:2bb3903f5387f72422145e1466b3ac3f7f0ef2e9960afa9bcd8961c5cbf8bd21", size = 1000532, upload-time = "2025-11-14T10:08:28.292Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/d2/29e5e536adc07bc3d33dd09f3f7cf844bf7b4981820dc2a91dd810f3c782/pyobjc_core-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:01c0cf500596f03e21c23aef9b5f326b9fb1f8f118cf0d8b66749b6cf4cbb37a", size = 677370, upload-time = "2025-11-14T09:33:05.273Z" },
{ url = "https://files.pythonhosted.org/packages/1b/f0/4b4ed8924cd04e425f2a07269943018d43949afad1c348c3ed4d9d032787/pyobjc_core-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:177aaca84bb369a483e4961186704f64b2697708046745f8167e818d968c88fc", size = 719586, upload-time = "2025-11-14T09:33:53.302Z" },
{ url = "https://files.pythonhosted.org/packages/25/98/9f4ed07162de69603144ff480be35cd021808faa7f730d082b92f7ebf2b5/pyobjc_core-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:844515f5d86395b979d02152576e7dee9cc679acc0b32dc626ef5bda315eaa43", size = 670164, upload-time = "2025-11-14T09:34:37.458Z" },
{ url = "https://files.pythonhosted.org/packages/62/50/dc076965c96c7f0de25c0a32b7f8aa98133ed244deaeeacfc758783f1f30/pyobjc_core-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:453b191df1a4b80e756445b935491b974714456ae2cbae816840bd96f86db882", size = 712204, upload-time = "2025-11-14T09:35:24.148Z" },
]
[[package]]
name = "pyobjc-framework-cocoa"
version = "12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyobjc-core" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/a3/16ca9a15e77c061a9250afbae2eae26f2e1579eb8ca9462ae2d2c71e1169/pyobjc_framework_cocoa-12.1.tar.gz", hash = "sha256:5556c87db95711b985d5efdaaf01c917ddd41d148b1e52a0c66b1a2e2c5c1640", size = 2772191, upload-time = "2025-11-14T10:13:02.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/31/0c2e734165abb46215797bd830c4bdcb780b699854b15f2b6240515edcc6/pyobjc_framework_cocoa-12.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5a3dcd491cacc2f5a197142b3c556d8aafa3963011110102a093349017705118", size = 384689, upload-time = "2025-11-14T09:41:41.478Z" },
{ url = "https://files.pythonhosted.org/packages/23/3b/b9f61be7b9f9b4e0a6db18b3c35c4c4d589f2d04e963e2174d38c6555a92/pyobjc_framework_cocoa-12.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:914b74328c22d8ca261d78c23ef2befc29776e0b85555973927b338c5734ca44", size = 388843, upload-time = "2025-11-14T09:42:05.719Z" },
{ url = "https://files.pythonhosted.org/packages/59/bb/f777cc9e775fc7dae77b569254570fe46eb842516b3e4fe383ab49eab598/pyobjc_framework_cocoa-12.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:03342a60fc0015bcdf9b93ac0b4f457d3938e9ef761b28df9564c91a14f0129a", size = 384932, upload-time = "2025-11-14T09:42:29.771Z" },
{ url = "https://files.pythonhosted.org/packages/58/27/b457b7b37089cad692c8aada90119162dfb4c4a16f513b79a8b2b022b33b/pyobjc_framework_cocoa-12.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6ba1dc1bfa4da42d04e93d2363491275fb2e2be5c20790e561c8a9e09b8cf2cc", size = 388970, upload-time = "2025-11-14T09:42:53.964Z" },
]
[[package]]
name = "rich"
version = "14.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
]
[[package]]
name = "scipy"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" },
{ url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" },
{ url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" },
{ url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" },
{ url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" },
{ url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" },
{ url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" },
{ url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" },
{ url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" },
{ url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" },
{ url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" },
{ url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" },
{ url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" },
{ url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" },
{ url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" },
{ url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" },
{ url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" },
{ url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" },
{ url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" },
{ url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" },
{ url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" },
{ url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" },
{ url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" },
{ url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" },
{ url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" },
{ url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" },
{ url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" },
{ url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" },
{ url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" },
{ url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" },
{ url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" },
{ url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" },
{ url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" },
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
]
[[package]]
name = "screeninfo"
version = "0.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cython", marker = "sys_platform == 'darwin'" },
{ name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ec/bb/e69e5e628d43f118e0af4fc063c20058faa8635c95a1296764acc8167e27/screeninfo-0.8.1.tar.gz", hash = "sha256:9983076bcc7e34402a1a9e4d7dabf3729411fd2abb3f3b4be7eba73519cd2ed1", size = 10666, upload-time = "2022-09-09T11:35:23.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/bf/c5205d480307bef660e56544b9e3d7ff687da776abb30c9cb3f330887570/screeninfo-0.8.1-py3-none-any.whl", hash = "sha256:e97d6b173856edcfa3bd282f81deb528188aff14b11ec3e195584e7641be733c", size = 12907, upload-time = "2022-09-09T11:35:21.351Z" },
]
[[package]]
name = "skia-pathops"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4a/f6/ab37d6fa21f25965d4ad059745c76f13ddfb92a2c06a842a42ad77961c24/skia_pathops-0.9.2.tar.gz", hash = "sha256:4b6d8459f6f4a69282cb26fca0c2bb0b321cc58a9bf9cc6579a52a391edc0319", size = 65115346, upload-time = "2026-02-16T10:30:22.753Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/10/45af3ea2dee706a340601da315e450c449f76aaa18651cfe2677b0c65161/skia_pathops-0.9.2-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:c7a925b919c050df1e0545d26a69b64d9c8e1959e4dca73b06df088bf01d30f0", size = 2897494, upload-time = "2026-02-16T10:30:00.265Z" },
{ url = "https://files.pythonhosted.org/packages/b4/20/711d733b252696aedd78175e0f60acd42299de680d5a50a2750b96578841/skia_pathops-0.9.2-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d94c18a60b1e56910240a65dcc841cca37067f6432a4881d20baff17b6d77915", size = 3300314, upload-time = "2026-02-16T10:30:02.53Z" },
{ url = "https://files.pythonhosted.org/packages/95/47/bccbdb7ee12bbcc43c1051f9fd6933d16e3ffc8f20263155bd03709bde9e/skia_pathops-0.9.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b28883f209a0df84da14e980e7a07c55ed93b913955694d90ddef2f29a261b", size = 2999370, upload-time = "2026-02-16T10:30:04.445Z" },
{ url = "https://files.pythonhosted.org/packages/5e/55/d5a1925d61ab683aaf08a3a80749371991f82fdeb53e3dbeae0046ce80ac/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8122e50e03472657781c951f3019d9b85d9b1cbc93ebac635b673f99b18895b6", size = 3955374, upload-time = "2026-02-16T10:30:06.308Z" },
{ url = "https://files.pythonhosted.org/packages/36/49/c81e1a551726b8153c7a600b041e4480ac85db13985b7b260a01d85e0a6c/skia_pathops-0.9.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f8c72c70e57dd6655f880ae63642ff26753bb888f4c6411709a12cc10bee005e", size = 4316667, upload-time = "2026-02-16T10:30:08.326Z" },
{ url = "https://files.pythonhosted.org/packages/bb/b4/484ba09325c75d4470578b3f16bfe5a30cc348ece2566df59702c487084c/skia_pathops-0.9.2-cp310-abi3-win32.whl", hash = "sha256:3b9ef6629d2c8cb8604a7e22d4d2fe2c0160788b9792145a2cf0f968ec245751", size = 1450912, upload-time = "2026-02-16T10:30:09.762Z" },
{ url = "https://files.pythonhosted.org/packages/ff/96/bd1bfaf5335242a86d8af940ff2124861e9bc6a5520cf96fda374fd10945/skia_pathops-0.9.2-cp310-abi3-win_amd64.whl", hash = "sha256:9ae72d801b5f4c5dbb937fb25476a12fa2da70860f28dd637da2b529dbd8271c", size = 1782901, upload-time = "2026-02-16T10:30:11.546Z" },
]
[[package]]
name = "soupsieve"
version = "2.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
]
[[package]]
name = "srt"
version = "3.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/b7/4a1bc231e0681ebf339337b0cd05b91dc6a0d701fa852bb812e244b7a030/srt-3.5.3.tar.gz", hash = "sha256:4884315043a4f0740fd1f878ed6caa376ac06d70e135f306a6dc44632eed0cc0", size = 28296, upload-time = "2023-03-28T02:35:44.007Z" }
[[package]]
name = "svgelements"
version = "1.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5d/29/1c93c94a2289675ba2ff898612f9c9a03f46d69f253bdf4da0dfc08a599d/svgelements-1.9.6.tar.gz", hash = "sha256:7c02ad6404cd3d1771fd50e40fbfc0550b0893933466f86a6eb815f3ba3f37f7", size = 162145, upload-time = "2023-08-17T02:01:51.822Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/2c/6c9bb53db56c8a12a736d2158a8b842a5993b96daabc29d90a098e840280/svgelements-1.9.6-py2.py3-none-any.whl", hash = "sha256:8a5cf2cc066d98e713d5b875b1d6e5eeb9b92e855e835ebd7caab2713ae1dcad", size = 137856, upload-time = "2023-08-17T02:01:48.76Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
]
+3 -4
View File
@@ -7,13 +7,12 @@
"dev": "concurrently -n site,cms -c blue,magenta \"pnpm --filter site dev\" \"pnpm --filter cms develop\"", "dev": "concurrently -n site,cms -c blue,magenta \"pnpm --filter site dev\" \"pnpm --filter cms develop\"",
"dev:site": "pnpm --filter site dev", "dev:site": "pnpm --filter site dev",
"dev:cms": "pnpm --filter cms develop", "dev:cms": "pnpm --filter cms develop",
"build": "pnpm run build:site && pnpm run build:cms",
"build:site": "pnpm --filter site build", "build:site": "pnpm --filter site build",
"build:cms": "pnpm --filter cms build", "build:cms": "pnpm --filter cms build",
"lighthouse": "pnpm --filter site lighthouse",
"lint": "biome check .", "lint": "biome check .",
"lint:fix": "biome check --write .", "lint:fix": "biome check --write ."
"legacy:generate:llms": "node scripts/generate-llms.mjs",
"legacy:generate:og": "node scripts/generate-og-images.mjs",
"legacy:build": "npm run legacy:generate:llms && npm run legacy:generate:og && zola build"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^2.5.11", "@biomejs/biome": "^2.5.11",
+1455 -4
View File
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
// 404 — Document Not Found
.notfound-wrap {
min-height: 60dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 3rem 0;
}
.case-notice {
max-width: 480px;
text-align: center;
}
.case-notice-ref {
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent-color);
margin-bottom: 1.5rem;
}
.case-notice-title {
margin-top: 0;
}
.case-notice-body {
color: var(--text-muted);
max-width: none;
}
.case-notice-links {
display: flex;
justify-content: center;
gap: 1.5rem;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
font-family: var(--font-mono);
font-size: 0.9rem;
a {
color: var(--text-color);
&:hover {
color: var(--accent-color);
}
}
}
-611
View File
@@ -1,611 +0,0 @@
// Article details and content styling
.reading-progress-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background-color: transparent;
z-index: 1000;
pointer-events: none;
}
.reading-progress-bar {
height: 100%;
width: 0%;
background-color: var(--accent-color);
transition: width 0.1s cubic-bezier(0.25, 0.8, 0.25, 1);
}
// Post Detail
.post-detail {
max-width: 800px;
margin: 0 auto;
padding-top: 4rem;
}
.post-header {
margin-bottom: 4rem;
}
.post-title {
margin-bottom: 1rem;
}
.post-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem 0.75rem;
color: var(--text-muted);
font-size: 0.9rem;
font-style: italic;
}
.post-meta-item {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.post-meta-label {
font-style: normal;
font-size: 0.75rem;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--accent-color);
opacity: 0.8;
}
.post-meta-sep {
color: var(--border-color);
font-style: normal;
user-select: none;
}
.post-meta-updated time {
color: var(--text-muted);
}
// Heading anchor links
.post-body {
h2,
h3 {
position: relative;
.heading-anchor {
display: inline-block;
margin-left: 0.5em;
color: var(--accent-color);
font-family: var(--font-mono);
font-weight: 400;
font-size: 0.75em;
opacity: 0;
text-decoration: none;
vertical-align: middle;
transition: opacity 0.15s ease;
user-select: none;
}
&:hover .heading-anchor {
opacity: 1;
}
}
}
// ──────────────────────────────────────────
// Series Navigation Box
// ──────────────────────────────────────────
.series-nav {
background: var(--surface-color);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-color);
padding: 1.25rem 1.5rem;
margin-bottom: 2rem;
}
.series-nav-header {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 0.4rem 0.75rem;
margin-bottom: 1rem;
}
.series-nav-eyebrow {
font-family: var(--font-mono);
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--accent-color);
opacity: 0.8;
}
.series-nav-title {
font-weight: 600;
font-size: 0.95rem;
color: var(--heading-color, var(--text-color));
flex: 1;
}
.series-nav-badge {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--text-muted);
background: var(--bg-color);
border: 1px solid var(--border-color);
padding: 0.1em 0.6em;
white-space: nowrap;
}
.series-nav-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.series-nav-item {
display: flex;
align-items: baseline;
gap: 0.75rem;
&.is-current {
.series-nav-index {
color: var(--accent-color);
}
.series-nav-label {
color: var(--text-color);
font-weight: 600;
}
}
}
.series-nav-index {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-muted);
opacity: 0.6;
flex-shrink: 0;
width: 1.5ch;
text-align: right;
}
.series-nav-label {
font-size: 0.9rem;
color: var(--text-muted);
text-decoration: none;
&:hover {
color: var(--accent-color);
}
}
.post-toc {
background: var(--surface-color);
border: 1px solid var(--border-color);
padding: 1.5rem;
margin-bottom: 3rem;
summary {
font-weight: 600;
cursor: pointer;
list-style: none; /* Hide default arrow */
display: flex;
align-items: center;
gap: 0.5rem;
&::before {
content: '↓';
color: var(--accent-color);
transition: transform 0.2s ease;
}
}
&[open] summary::before {
transform: rotate(180deg);
}
ul {
margin-top: 1rem;
margin-bottom: 0;
list-style-type: none;
padding-left: 1rem;
}
li {
margin-bottom: 0.5rem;
}
a {
color: var(--text-muted);
font-size: 0.95em;
&:hover {
color: var(--accent-color);
}
}
}
.post-tags-share {
display: flex;
flex-direction: column;
gap: 2rem;
margin-top: 4rem;
@media (min-width: 640px) {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.social-share {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
.share-label {
font-weight: 500;
color: var(--text-color);
margin-right: 0.25rem;
}
.share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--surface-color);
border: 1px solid var(--border-color);
color: var(--text-muted);
transition: var(--transition-smooth);
cursor: pointer;
&:hover {
color: var(--bg-color);
transform: translateY(-2px);
border-color: transparent;
&.x {
background: #000000;
color: #ffffff;
}
&.bluesky {
background: #0285ff;
}
&.linkedin {
background: #0077b5;
}
&.reddit {
background: #ff4500;
}
&.hackernews {
background: #ff6600;
}
&.email {
background: #6b7280;
}
}
}
}
.post-nav {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.post-nav-link {
display: flex;
flex-direction: column;
gap: 0.35rem;
align-items: center;
text-align: center;
max-width: 34ch;
}
.post-nav-label {
color: var(--accent-color);
font-size: 0.9rem;
font-weight: 500;
}
.post-nav-title {
font-size: 1rem;
line-height: 1.35;
color: var(--text-muted);
transition: color 0.2s ease;
}
.post-nav-link:hover .post-nav-title,
.post-nav-link:hover .post-nav-label {
color: var(--accent-color);
}
@media (max-width: 640px) {
.post-nav-title {
font-size: 0.95rem;
}
}
@media (min-width: 900px) {
.post-nav {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 2rem;
align-items: start;
}
.post-nav-prev {
justify-self: start;
align-items: flex-start;
text-align: left;
}
.post-nav-next {
justify-self: end;
align-items: flex-end;
text-align: right;
}
}
// ──────────────────────────────────────────
// Sidebar TOC Layout
// ──────────────────────────────────────────
.post-layout {
display: contents;
@media (min-width: 1200px) {
display: grid;
grid-template-columns: minmax(0, 800px) 240px;
gap: 3rem;
align-items: start;
max-width: 1100px;
margin: 0 auto;
}
}
@media (min-width: 1200px) {
.post-detail {
max-width: none;
margin: 0;
}
// Hide inline TOC — sidebar takes over
.post-toc {
display: none !important;
}
}
.post-toc-sidebar {
display: none;
@media (min-width: 1200px) {
display: block;
position: sticky;
top: 5.5rem;
max-height: calc(100vh - 7rem);
overflow-y: auto;
padding-top: 4rem; // align with article content start
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border-color);
}
}
}
.toc-nav {
ul {
list-style: none;
padding: 0;
margin: 0;
// Nested level (h2 children)
ul {
padding-left: 1rem;
margin-top: 0.25rem;
}
}
li {
margin-bottom: 0.35rem;
}
a {
display: block;
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.4;
padding: 0.2rem 0;
border-left: 2px solid transparent;
padding-left: 0.75rem;
transition:
color 0.2s ease,
border-color 0.2s ease;
text-decoration: none;
&:hover {
color: var(--accent-color);
}
&.toc-active {
color: var(--accent-color);
border-left-color: var(--accent-color);
}
}
}
.toc-sidebar-title {
font-size: 0.75rem;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-muted);
margin: 0 0 1rem 0;
padding-left: 0.75rem;
}
// ──────────────────────────────────────────
// Media Wrappers (YouTube, etc)
// ──────────────────────────────────────────
.youtube-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
border: 1px solid var(--border-color);
margin: 2rem 0;
background: #000;
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 0;
}
}
// ──────────────────────────────────────────
// Callouts / Admonitions
// ──────────────────────────────────────────
.callout {
margin: 2rem 0;
padding: 1.25rem 1.5rem;
border-left: 4px solid var(--callout-color, var(--border-color));
background: var(--surface-color);
color: var(--text-color);
font-size: 0.95rem;
line-height: 1.6;
.callout-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--callout-color, var(--heading-color));
}
.callout-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
.callout-title {
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.85rem;
}
.callout-content {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
// Variants
&.callout-note {
--callout-color: #3b82f6; // Blue
background: rgba(59, 130, 246, 0.05);
}
&.callout-warning {
--callout-color: #f59e0b; // Amber
background: rgba(245, 158, 11, 0.05);
}
&.callout-tip {
--callout-color: #16a34a; // Green
background: rgba(22, 163, 74, 0.05);
}
&.callout-danger {
--callout-color: #ef4444; // Red
background: rgba(239, 68, 68, 0.05);
}
}
.comments {
margin-top: 4rem;
}
// ──────────────────────────────────────────
// Related Posts
// ──────────────────────────────────────────
.related-posts-section {
margin-top: 4rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
h3 {
margin-bottom: 2rem;
font-size: 1.5rem;
color: var(--heading-color);
letter-spacing: -0.02em;
}
.related-posts-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
@media (min-width: 768px) {
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2.5rem;
}
// Override some constraints from feed-page cards when inside the grid
.feed-item {
border-bottom: none !important;
padding-bottom: 0 !important;
background: var(--surface-color);
border: 1px solid var(--border-color);
padding: 1.5rem !important;
height: 100%;
display: flex;
flex-direction: column;
transition: var(--transition-smooth);
&:hover {
border-color: var(--accent-color);
}
.item-snippet {
flex: 1;
}
}
}
}
-304
View File
@@ -1,304 +0,0 @@
// Post Feed Page Container
.feed-page {
max-width: 800px;
margin: 0 auto;
}
// Post Feed
.post-feed {
display: flex;
flex-direction: column;
margin-top: 1rem;
}
.post-feed .feed-item {
padding: 2rem 0;
border-bottom: 1px solid var(--border-color);
&:first-child {
padding-top: 0;
}
}
// Homepage "View all posts" CTA
.view-all-posts {
display: flex;
justify-content: center;
margin-top: 3rem;
}
.btn-view-all {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.5rem;
border: 1px solid var(--accent-color);
border-radius: 2px;
font-family: var(--font-mono);
font-size: 0.875rem;
color: var(--accent-color);
background: transparent;
text-decoration: none;
transition: var(--transition-smooth);
letter-spacing: 0.04em;
&::after {
content: '→';
transition: transform 0.2s ease;
}
&:hover {
background: var(--accent-color);
color: var(--bg-color);
&::after {
transform: translateX(4px);
}
}
}
.feed-item {
display: block;
}
// Homepage post list (post-card.html) — a numbered index of entries
.feed-heading {
font-size: 1.1rem;
margin: 0 0 1.5rem;
}
.post-feed-list {
display: flex;
flex-direction: column;
}
.feed-item.card {
display: flex;
gap: 1.25rem;
border-bottom: 1px solid var(--border-color);
padding: 2rem 0;
&:first-child {
padding-top: 0;
}
.item-title {
font-size: 1.25rem;
margin-bottom: 0.5rem;
font-weight: 600;
a {
transition: color 0.2s ease;
}
}
&:hover .item-title a {
color: var(--accent-color);
}
}
.item-index {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--text-muted);
padding-top: 0.3rem;
flex-shrink: 0;
}
.item-body {
min-width: 0;
flex: 1;
}
.item-meta {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 1rem;
font-family: var(--font-mono);
}
.item-meta-sep {
opacity: 0.5;
}
.item-snippet {
color: var(--text-color);
opacity: 0.9;
margin-bottom: 1rem;
p {
margin: 0;
}
}
.item-footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 0.75rem;
font-size: 0.85rem;
}
.item-readmore {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--accent-color);
.readmore-arrow {
transition: transform 0.2s ease;
}
&:hover .readmore-arrow {
transform: translateX(4px);
}
}
.item-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
.item-link {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item-date-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.item-date {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.item-reading-time {
color: var(--text-muted);
font-size: 0.85rem;
font-family: var(--font-mono);
}
.item-title {
margin: 0;
font-size: clamp(1.5rem, 2vw, 2rem);
transition: color 0.2s ease;
}
.item-link:hover .item-title {
color: var(--accent-color);
}
.item-desc {
margin: 0;
color: var(--text-muted);
}
// ──────────────────────────────────────────
// Pagination
// ──────────────────────────────────────────
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 5rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
font-family: var(--font-mono);
}
.pagination-pages {
display: flex;
align-items: center;
gap: 0.25rem;
}
// Shared base for arrows + numbers
.pagination-arrow,
.pagination-num {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
padding: 0 0.5rem;
border-radius: 2px;
border: 1px solid var(--border-color);
background: var(--surface-color);
color: var(--text-muted);
font-size: 0.875rem;
text-decoration: none;
transition: var(--transition-smooth);
cursor: pointer;
user-select: none;
&:hover {
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateY(-1px);
}
}
// Active / current page
.pagination-num.is-current {
background: var(--accent-color);
border-color: var(--accent-color);
color: #fff;
font-weight: 600;
cursor: default;
pointer-events: none;
&:hover {
transform: none;
}
}
// Disabled arrow (no prev/next page)
.pagination-arrow.is-disabled {
opacity: 0.3;
cursor: not-allowed;
pointer-events: none;
&:hover {
border-color: var(--border-color);
color: var(--text-muted);
transform: none;
}
}
// Ellipsis gap marker
.pagination-ellipsis {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.25rem;
height: 2.25rem;
color: var(--text-muted);
font-size: 0.875rem;
letter-spacing: 0.05em;
}
// "Page N of M" helper text
.pagination-meta {
text-align: center;
margin-top: 1rem;
margin-bottom: 0;
font-size: 0.8rem;
color: var(--text-muted);
font-family: var(--font-mono);
letter-spacing: 0.05em;
}
-10
View File
@@ -1,10 +0,0 @@
// Footer
.site-footer {
padding: 2rem 0 3rem;
border-top: 3px solid var(--text-color);
color: var(--text-muted);
font-size: 0.85rem;
font-family: var(--font-mono);
display: flex;
justify-content: space-between;
}
-183
View File
@@ -1,183 +0,0 @@
// Component: Header
.site-header {
padding: 1.25rem 0;
position: sticky;
top: 0;
z-index: 50;
background-color: var(--bg-color);
border-bottom: 2px solid var(--text-color);
}
.nav-container {
display: flex;
justify-content: space-between;
align-items: center;
}
// Right side wrapper (links + controls)
.nav-right {
display: flex;
align-items: center;
gap: 1rem;
}
// Always-visible icon controls
.nav-controls {
display: flex;
align-items: center;
gap: 0.5rem;
}
.search-toggle {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0.35rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--transition-smooth);
&:hover {
color: var(--accent-color);
}
}
// Hamburger button — hidden on desktop, shown on mobile
.nav-toggle {
display: none;
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0.25rem;
align-items: center;
justify-content: center;
.close-icon {
display: none;
}
&[aria-expanded='true'] {
.hamburger-icon {
display: none;
}
.close-icon {
display: block;
}
}
}
@media (max-width: 640px) {
.nav-toggle {
display: flex;
}
// Hide the site name when the menu is open
.nav-container:has(#nav-toggle[aria-expanded='true']) .brand-link {
display: none;
}
// Make nav-right fill the full width so menu items span the container
.nav-container:has(#nav-toggle[aria-expanded='true']) .nav-right {
width: 100%;
}
.nav-right {
flex-wrap: wrap;
justify-content: flex-end;
}
.nav-links {
display: flex !important;
flex-direction: column !important;
align-items: flex-start !important;
gap: 0 !important;
width: 100%;
order: 10; // push below nav-controls
padding: 0;
margin-top: 0;
border-top: 1px solid transparent;
max-height: 0;
opacity: 0;
overflow: hidden;
transition:
max-height 0.3s cubic-bezier(0.16, 1, 0.3, 1),
opacity 0.2s ease;
&.is-open {
padding: 1rem 0;
margin-top: 0.5rem;
border-top-color: var(--border-color);
max-height: 320px;
opacity: 1;
}
.nav-item {
width: 100%;
padding: 0.75rem 0;
font-size: 1rem;
border-bottom: 1px solid var(--border-color);
&::after {
display: none;
}
}
}
}
// Brand reads like a running header on a printed document — quiet,
// small, letter-spaced — not competing with the masthead title below it.
.brand-text {
font-family: var(--font-mono);
font-weight: 600;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
.brand-link:hover .brand-text {
color: var(--accent-color);
}
.nav-links {
display: flex;
align-items: center;
gap: 1.75rem;
}
.nav-item {
font-family: var(--font-mono);
font-weight: 500;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-color);
position: relative;
}
.nav-item::after {
content: '';
position: absolute;
width: 100%;
height: 2px;
bottom: -4px;
left: 0;
background-color: var(--accent-color);
transform: scaleX(0);
transform-origin: left;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.nav-item:hover::after {
transform: scaleX(1);
}
.nav-item.is-active {
color: var(--accent-color);
&::after {
transform: scaleX(1);
}
}
-70
View File
@@ -1,70 +0,0 @@
// Hero Section
.hero {
padding: 8rem 0 6rem 0;
max-width: 800px;
}
.hero-compact {
padding-bottom: 2rem;
}
.hero-title {
color: var(--text-color);
}
.hero-subtitle {
font-size: clamp(1.1rem, 1.5vw, 1.3rem);
color: var(--text-muted);
}
// Homepage masthead — spans the full width of .site-container, matching the navbar
.masthead {
padding: 4rem 0 3rem;
}
.masthead-kicker {
font-family: var(--font-mono);
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent-color);
margin-bottom: 1rem;
}
.masthead-title {
margin-top: 0;
}
// Colophon-style contact line — quiet, small, middot-separated
.masthead-links {
display: flex;
flex-wrap: wrap;
row-gap: 0.4rem;
margin-top: 2rem;
font-family: var(--font-mono);
font-size: 0.8rem;
letter-spacing: 0.02em;
a {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--text-muted);
transition: var(--transition-smooth);
svg {
flex-shrink: 0;
}
&:hover {
color: var(--accent-color);
}
&:not(:last-child)::after {
content: '·';
margin: 0 0.75rem;
color: var(--text-muted);
opacity: 0.5;
}
}
}
-37
View File
@@ -1,37 +0,0 @@
// Layout Constraints
.site-container {
width: 100%;
max-width: 1400px;
margin: 0 auto;
padding: 0 5vw;
display: flex;
flex-direction: column;
min-height: 100dvh;
box-sizing: border-box;
}
// Constrain all media & tables to their container
img,
video,
iframe,
table,
svg {
max-width: 100%;
}
table {
display: block;
overflow-x: auto;
@media (max-width: 640px) {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
// Main Content
.main-content {
flex-grow: 1;
padding-bottom: 5rem;
}
-107
View File
@@ -1,107 +0,0 @@
// Archive & Tags list pages
.list-page-header {
margin-bottom: 3rem;
}
.list-page-title {
font-size: clamp(2.5rem, 5vw, 4.5rem);
}
.term-hash {
color: var(--text-muted);
}
.tag-description {
max-width: 65ch;
margin: 0.75rem 0 0;
color: var(--text-muted);
font-size: 1.05rem;
}
.archive-list {
max-width: 65ch;
margin: 0 auto;
}
.archive-year {
margin-bottom: 2rem;
}
.archive-year-title {
font-size: 1.5rem;
margin-top: 2rem;
margin-bottom: 1rem;
}
.archive-items {
list-style: none;
padding-left: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.archive-item {
display: flex;
gap: 1rem;
align-items: baseline;
}
.archive-date {
font-family: var(--font-mono);
color: var(--text-muted);
font-size: 0.95rem;
white-space: nowrap;
}
.archive-link {
color: var(--accent-color);
font-weight: 500;
font-size: 1.05rem;
}
// Tags index
.tags-page {
max-width: 65ch;
margin: 0 auto;
padding-top: 4rem;
text-align: center;
}
.tags-page-header {
margin-bottom: 3rem;
}
.page-title {
font-size: 2rem;
font-weight: 600;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 2rem;
justify-content: center;
align-items: center;
}
.tag-link {
font-size: 1.1rem;
color: var(--text-color);
font-weight: 400;
display: inline-flex;
align-items: flex-start;
&:hover {
color: var(--accent-color);
}
}
.tag-count {
font-size: 0.7em;
color: var(--text-muted);
margin-left: 0.15rem;
margin-top: 0.1rem;
}
-58
View File
@@ -1,58 +0,0 @@
// Global Reset & Base
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: var(--font-sans);
font-size: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
background-color: var(--bg-color);
color: var(--text-color);
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition:
background-color 0.3s ease,
color 0.3s ease;
// Firefox
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
// Webkit
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--scrollbar-track);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 0; // hard edges = terminal feel
border: 1px solid var(--scrollbar-track);
&:hover {
background: var(--scrollbar-thumb-hover);
}
}
body {
line-height: 1.7;
min-height: 100dvh;
display: flex;
flex-direction: column;
overflow-wrap: break-word; // break long words instead of overflowing
word-break: break-word;
}
a {
color: inherit;
text-decoration: none;
transition: var(--transition-smooth);
}
-64
View File
@@ -1,64 +0,0 @@
// Scroll to Top Floating Button
.scroll-to-top {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
border-radius: 2px;
background: var(--bg-color);
color: var(--accent-color);
border: 1px solid var(--accent-color);
cursor: pointer;
z-index: 100;
// Hidden state by default
opacity: 0;
visibility: hidden;
transform: translateY(1rem);
transition:
transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
opacity 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
visibility 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
box-shadow 0.2s ease-out;
// Visible state triggered by JS
&.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
@media (hover: hover) {
&:hover {
background: var(--accent-color);
color: var(--bg-color);
transform: translateY(-0.25rem);
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent-color) 20%, transparent);
}
&:hover svg {
transform: translateY(-2px);
}
}
&:focus-visible {
outline: 2px solid var(--accent-color);
outline-offset: 4px;
}
svg {
transition: transform 0.2s ease;
}
// Responsive padding for mobile devices
@media (max-width: 768px) {
bottom: 1.5rem;
right: 1.5rem;
width: 2.75rem;
height: 2.75rem;
}
}
-149
View File
@@ -1,149 +0,0 @@
// Search Modal Styles
.search-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
justify-content: center;
align-items: flex-start;
padding-top: 10vh;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease,
visibility 0.2s ease;
&.active {
opacity: 1;
visibility: visible;
.search-modal {
transform: translateY(0) scale(1);
}
}
}
.search-modal {
width: 90%;
max-width: 600px;
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 2px;
box-shadow: 0 12px 32px var(--shadow-color);
transform: translateY(-20px) scale(0.98);
transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: flex;
flex-direction: column;
overflow: hidden;
}
.search-header {
display: flex;
align-items: center;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--border-color);
background: var(--surface-color);
svg {
color: var(--text-muted);
margin-right: 0.75rem;
}
}
#search-input {
flex: 1;
background: transparent;
border: none;
color: var(--text-color);
font-family: var(--font-mono);
font-size: 1rem;
outline: none;
&::placeholder {
color: var(--text-muted);
}
}
.search-footer {
padding: 0.5rem 1.25rem;
background: var(--surface-color);
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.75rem;
color: var(--text-muted);
kbd {
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 0.1rem 0.3rem;
font-family: var(--font-mono);
font-size: 0.7rem;
margin: 0 0.2rem;
}
}
.search-results {
max-height: 50vh;
overflow-y: auto;
padding: 0.5rem;
// Custom scrollbar
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 4px;
}
.search-result-item {
display: flex;
flex-direction: column;
padding: 0.75rem 1rem;
border-radius: 2px;
text-decoration: none;
color: var(--text-color);
transition: background-color 0.15s ease;
border-left: 2px solid transparent;
&:hover,
&.selected {
background-color: var(--surface-color);
border-left-color: var(--accent-color);
}
.result-title {
font-weight: 500;
color: var(--accent-color);
margin-bottom: 0.25rem;
}
.result-summary {
font-size: 0.85rem;
color: var(--text-muted);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.no-results {
padding: 2rem;
text-align: center;
color: var(--text-muted);
font-style: italic;
}
}
-31
View File
@@ -1,31 +0,0 @@
// Component: Theme Toggle
.theme-toggle {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: var(--transition-smooth);
svg {
width: 20px;
height: 20px;
}
}
html.dark .sun-icon {
display: block;
}
html.dark .moon-icon {
display: none;
}
html:not(.dark) .sun-icon {
display: none;
}
html:not(.dark) .moon-icon {
display: block;
}
-55
View File
@@ -1,55 +0,0 @@
/*
* Cross-Document View Transitions
* Enabled via <meta name="view-transition" content="same-origin" />
*/
// Disable default CSS transitions on the whole page during a view transition
// so they don't fight with the View Transition API animation.
html::view-transition-group(*) {
animation-duration: 0.3s;
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
// Custom animation for the old page leaving
@keyframes fade-out-scale {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.98);
}
}
// Custom animation for the new page entering
@keyframes fade-in-scale {
from {
opacity: 0;
transform: scale(1.02);
}
to {
opacity: 1;
transform: scale(1);
}
}
// Apply the custom animations to the root view transition pseudo-elements
html::view-transition-old(root) {
animation: fade-out-scale 0.25s cubic-bezier(0.4, 0, 1, 1) both;
}
html::view-transition-new(root) {
animation: fade-in-scale 0.35s cubic-bezier(0, 0, 0.2, 1) both;
// Delay slightly so the old page fades down first before the new one pops over
animation-delay: 0.05s;
}
// Respect user preference for reduced motion
@media (prefers-reduced-motion: reduce) {
html::view-transition-group(*),
html::view-transition-old(*),
html::view-transition-new(*) {
animation: none !important;
}
}
-195
View File
@@ -1,195 +0,0 @@
// Typography Scale (Fluid)
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-display);
font-weight: 700;
line-height: 1.15;
letter-spacing: -0.01em;
margin-top: 2em;
margin-bottom: 0.75em;
overflow-wrap: break-word;
word-break: break-word;
text-wrap: balance;
}
h1 {
font-size: clamp(1.75rem, 5vw, 4.5rem);
margin-top: 0;
}
h2 {
font-size: clamp(1.35rem, 3vw, 2.5rem);
}
h3 {
font-size: clamp(1.2rem, 2vw, 1.8rem);
}
h4 {
font-size: clamp(1.05rem, 1.5vw, 1.3rem);
}
h5 {
font-size: clamp(1rem, 1.25vw, 1.15rem);
}
h6 {
font-size: clamp(0.9rem, 1vw, 1rem);
}
p,
ul,
ol,
figure,
blockquote,
pre,
table {
margin-bottom: 1.5rem;
}
hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 2rem 0;
}
p {
max-width: 65ch; // Optimal reading width
text-wrap: pretty;
}
// Footnotes
.footnote-reference {
font-size: 0.75em;
vertical-align: super;
line-height: 0;
a {
color: var(--accent-color);
font-family: var(--font-mono);
padding: 0 0.15em;
&:hover {
text-decoration: underline;
}
}
}
.footnote-definition {
display: flex;
gap: 0.75rem;
margin-bottom: 0.75rem;
font-size: 0.875rem;
color: var(--text-muted);
border-left: 2px solid var(--border-color);
padding-left: 1rem;
p {
margin: 0;
max-width: none;
}
}
.footnote-definition-label {
flex-shrink: 0;
font-family: var(--font-mono);
color: var(--accent-color);
font-size: 0.8em;
margin-top: 0.1em;
}
.footnote-backref {
font-family: var(--font-mono);
color: var(--accent-color);
font-size: 0.9em;
text-decoration: none;
opacity: 0.7;
transition: opacity 0.2s ease;
&:hover {
opacity: 1;
}
}
code,
pre {
font-family: var(--font-mono);
font-size: 0.9em;
font-feature-settings:
'liga' 1,
'calt' 1;
font-variant-ligatures: contextual;
}
.code-block-wrapper {
position: relative;
}
pre {
padding: 1.25rem;
border-radius: 2px;
overflow-x: auto;
max-width: 100%;
border: 1px solid var(--border-color);
background: #282828; // Gruvbox Dark bg0 (matches [markdown.highlighting] theme)
// Scrollbars visible on desktop, hidden on mobile
@media (max-width: 640px) {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
font-size: 0.82em;
}
}
.copy-code-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: rgba(235, 219, 178, 0.1);
color: #ebdbb2;
border: 1px solid rgba(235, 219, 178, 0.2);
border-radius: 2px;
cursor: pointer;
opacity: 1;
transition:
background 0.2s ease,
color 0.2s ease;
svg {
width: 16px;
height: 16px;
}
&:hover {
background: rgba(235, 219, 178, 0.2);
color: #fbf1c7;
}
&.copied {
background: var(--accent-color);
border-color: var(--accent-color);
color: var(--bg-color);
}
}
// Button is always visible, no hover-to-reveal needed
code {
background: var(--surface-color);
padding: 0.2em 0.4em;
border-radius: 2px;
color: var(--accent-color);
border: 1px solid var(--border-color);
}
pre code {
background: transparent;
color: inherit;
padding: 0;
border: none;
}
-39
View File
@@ -1,39 +0,0 @@
// Tag chip — a small filed label, shared by post-card and post-tags
.tag {
display: inline-block;
background: var(--surface-color);
border: 1px solid var(--border-color);
padding: 0.2rem 0.6rem;
border-radius: 2px;
font-size: 0.8rem;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-muted);
transition: var(--transition-smooth);
&:hover {
color: var(--accent-color);
border-color: var(--accent-color);
}
}
// Heavy hairline rule — the site's one recurring structural device
// (masthead, between list entries, above case-reference blocks)
.rule {
border: none;
border-top: 3px solid var(--text-color);
}
// Utilities / Interactions
.hover-scale {
transition: transform 140ms ease-out;
&:hover {
transform: scale(1.02);
}
&:active {
transform: scale(0.98);
}
}
-42
View File
@@ -1,42 +0,0 @@
// Design Variables — "Case File": a declassified incident-report aesthetic.
// One accent only; flat colors + hairline borders, no gradients or glass blur.
:root {
--bg-color: #e9e9e2; // paper (cool bone, not warm cream)
--surface-color: #dedecf; // slightly recessed paper — cards, code blocks
--text-color: #17181c; // ink
--text-muted: #68675f; // faded ink
--accent-color: #b42318; // classified-stamp red
--accent-hover: #8e1b12;
--border-color: rgba(23, 24, 28, 0.16);
--shadow-color: rgba(23, 24, 28, 0.08);
--font-sans: 'Source Serif 4', Georgia, serif; // body copy
--font-display: 'IBM Plex Serif', Georgia, serif; // headings, masthead
--font-mono: 'JetBrains Mono', monospace; // case-reference lines, tags, code
--transition-smooth:
transform 0.2s cubic-bezier(0.16, 1, 0.3, 1),
color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
background-color 0.2s cubic-bezier(0.16, 1, 0.3, 1),
border-color 0.2s cubic-bezier(0.16, 1, 0.3, 1);
--scrollbar-track: #dedecf;
--scrollbar-thumb: #b0afa2;
--scrollbar-thumb-hover: #b42318;
}
// Dark mode — same document, read at night
html.dark {
--bg-color: #131311;
--surface-color: #1b1a17;
--text-color: #e9e7de;
--text-muted: #8b8a80;
--accent-color: #ff5a46;
--accent-hover: #ff7c6b;
--border-color: rgba(233, 231, 222, 0.14);
--shadow-color: rgba(0, 0, 0, 0.5);
--scrollbar-track: #1b1a17;
--scrollbar-thumb: #3a382f;
--scrollbar-thumb-hover: #ff5a46;
}
-19
View File
@@ -1,19 +0,0 @@
// Main SCSS entry point
// Organized by feature and section for maintainability
@import 'variables';
@import 'reset';
@import 'typography';
@import 'layout';
@import 'header';
@import 'theme';
@import 'hero';
@import 'feed';
@import 'lists';
@import 'article';
@import 'footer';
@import 'search';
@import 'transitions';
@import 'utilities';
@import 'scroll-top';
@import '404';
-182
View File
@@ -1,182 +0,0 @@
// One-off migration: converts content/posts/*.md (legacy Zola posts) into
// content/writings/<slug>/index.md (new schema). Additive only — never
// touches content/posts/. Re-runnable: overwrites its own prior output.
import fs from "node:fs";
import path from "node:path";
import {
parse as parseToml,
stringify as stringifyToml,
TomlDate,
} from "smol-toml";
const root = process.cwd();
const postsDir = path.join(root, "content", "posts");
const writingsDir = path.join(root, "content", "writings");
const FRONTMATTER_RE = /^\+\+\+\r?\n([\s\S]*?)\r?\n\+\+\+\r?\n?([\s\S]*)$/;
function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) return walk(full);
return entry.isFile() && entry.name.endsWith(".md") ? [full] : [];
});
}
function slugFor(file) {
const rel = path.relative(postsDir, file).split(path.sep).join("/");
return rel.endsWith("/index.md")
? rel.slice(0, -"/index.md".length)
: rel.slice(0, -".md".length);
}
function compact(obj) {
const out = {};
for (const [key, value] of Object.entries(obj)) {
if (value === undefined || value === null || value === "") continue;
if (Array.isArray(value) && value.length === 0) continue;
if (
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Date)
) {
const nested = compact(value);
if (Object.keys(nested).length === 0) continue;
out[key] = nested;
continue;
}
out[key] = value;
}
return out;
}
function normalizeTag(tag) {
return tag.trim().toLowerCase().replace(/\s+/g, "-");
}
function toIsoDate(value) {
if (value instanceof Date)
return new TomlDate(value.toISOString().slice(0, 10));
if (typeof value === "string") return new TomlDate(value.slice(0, 10));
return undefined;
}
/** Converts the one unambiguous Zola shortcode in use (youtube embeds) to a
* plain HTML iframe, and strips Zola's `{% raw %}`/`{% endraw %}` escape
* tags (meaningless now — there's no template engine processing the body).
* Anything else Zola-specific (note/tip/warning/danger callouts) is left
* as-is: those need real Astro components before they can be translated,
* so they're flagged for manual follow-up instead of guessed at. */
function migrateBody(body) {
const flags = [];
const out = body
.replace(/\{%\s*raw\s*%\}\r?\n?/g, "")
.replace(/\{%\s*endraw\s*%\}\r?\n?/g, "")
.replace(/\{\{<\s*youtube\s+id="([^"]+)"[\s\S]*?\/>\s*\}\}/g, (_m, id) => {
return `<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/${id}" title="YouTube video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
});
if (/\{%\s*<?\/?(note|tip|warning|danger)>?\s*%\}/.test(out)) {
flags.push("callout-shortcode");
}
if (/\{\{|\{%/.test(out)) {
flags.push("unresolved-template-syntax");
}
return { body: out, flags };
}
const files = walk(postsDir).filter((f) => path.basename(f) !== "_index.md");
fs.mkdirSync(writingsDir, { recursive: true });
let migrated = 0;
const flaggedFiles = [];
const migratedSlugs = [];
for (const file of files) {
const raw = fs.readFileSync(file, "utf8");
const match = raw.match(FRONTMATTER_RE);
if (!match) continue;
const [, fmSource, body] = match;
const fm = parseToml(fmSource);
const slug = slugFor(file);
const { body: newBody, flags } = migrateBody(body.trim());
if (flags.length) flaggedFiles.push({ slug, flags });
const frontmatter = stringifyToml(
compact({
title: fm.title,
description: fm.description,
author: fm.author,
date: toIsoDate(fm.date),
updated: toIsoDate(fm.updated),
draft: fm.draft ?? false,
canonical_url: fm.canonical_url,
taxonomies: { tags: (fm.taxonomies?.tags ?? []).map(normalizeTag) },
extra: {
series: fm.extra?.series,
series_index: fm.extra?.series_index,
llms_description: fm.extra?.llms_description,
},
}),
);
const outDir = path.join(writingsDir, slug);
fs.mkdirSync(outDir, { recursive: true });
fs.writeFileSync(
path.join(outDir, "index.md"),
`+++\n${frontmatter}\n+++\n\n${newBody}\n`,
);
migrated += 1;
migratedSlugs.push(slug);
}
// Keep old /posts/<slug>/ URLs alive (RSS subscribers, inbound links) by
// redirecting to the new /writings/<slug>/ path. Regenerated idempotently
// inside marker comments so re-running this script doesn't duplicate or
// clobber the rest of sws.toml.
const swsPath = path.join(root, "sws.toml");
const BEGIN =
"# BEGIN migrated-posts-redirects (generated by scripts/migrate-posts-to-writings.mjs)";
const END = "# END migrated-posts-redirects";
const redirectBlock = [
BEGIN,
...migratedSlugs
.sort()
.flatMap((slug) => [
"[[advanced.redirects]]",
`source = "/posts/${slug}/"`,
`destination = "/writings/${slug}/"`,
"kind = 301",
"",
]),
END,
]
.join("\n")
.trimEnd();
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const swsSource = fs.readFileSync(swsPath, "utf8");
const markerRe = new RegExp(
`${escapeRegExp(BEGIN)}[\\s\\S]*?${escapeRegExp(END)}`,
);
const updatedSws = markerRe.test(swsSource)
? swsSource.replace(markerRe, redirectBlock)
: `${swsSource.trimEnd()}\n\n${redirectBlock}\n`;
fs.writeFileSync(swsPath, `${updatedSws.trimEnd()}\n`);
console.log(
`Migrated ${migrated} post(s) from content/posts/ to content/writings/.`,
);
console.log(`Wrote ${migratedSlugs.length} redirect(s) to sws.toml.`);
if (flaggedFiles.length) {
console.log(
"\nNeeds manual follow-up (Zola-specific syntax with no Astro equivalent yet):",
);
for (const { slug, flags } of flaggedFiles) {
console.log(` - ${slug}: ${flags.join(", ")}`);
}
}
-13
View File
@@ -1,13 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEalcjtBYJKwYBBAHaRw8BAQdA55X1mRCKw9DA35kCAy+7Z30ye0rUHWBZsl7O
Wmar+RS0IklhbiBBbmR3YXRpIDxhbmR3YXRpaWFuQGdtYWlsLmNvbT6IkwQTFgoA
OxYhBK1YqreiDYUjy/WaeR8g/tCqBbDaBQJqVyO0AhsDBQsJCAcCAiICBhUKCQgL
AgQWAgMBAh4HAheAAAoJEB8g/tCqBbDace8BAN1SpkEMSYll+XupKHiaIUwnhm4N
eS79G1i+345Gl6CyAQDjgn+3rzG6sexjKh+10EKiy5omsQEGzUW03s7gS0T1Drg4
BGpXI7QSCisGAQQBl1UBBQEBB0CXofBIYYXG2+dZqgllhsbkNPF4qlyfPgbRW+zb
7Kw3IAMBCAeIeAQYFgoAIBYhBK1YqreiDYUjy/WaeR8g/tCqBbDaBQJqVyO0AhsM
AAoJEB8g/tCqBbDauMkA/RG4iqtPZOI3DQzFJYu7SnxaD81vdP0LyTLKDQkynySu
AQDrlWyF8LRlad4iX1yQPCuOV0aUpTNq5kPlWurtX99/BQ==
=uKsS
-----END PGP PUBLIC KEY BLOCK-----
-17
View File
@@ -1,17 +0,0 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Contact: mailto:[email protected]
Expires: 2027-07-16T00:00:00.000Z
Encryption: https://andwati.com/.well-known/pgp-key.txt
Encryption: https://keys.openpgp.org/vks/v1/by-fingerprint/AD58AAB7A20D8523CBF59A791F20FED0AA05B0DA
Encryption: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xAD58AAB7A20D8523CBF59A791F20FED0AA05B0DA
Preferred-Languages: en
Canonical: https://andwati.com/.well-known/security.txt
-----BEGIN PGP SIGNATURE-----
iHUEARYKAB0WIQStWKq3og2FI8v1mnkfIP7QqgWw2gUCaljXlQAKCRAfIP7QqgWw
2qAoAP0Q7Cc0YsP2dvV7fqDlqcnjuJepQQSTZBz7ehkPyjGV5gEA3K9ZqQmuI2ea
KKADWxuKTTY43PBmuV9rMqLm8ZOPlAY=
=NUkY
-----END PGP SIGNATURE-----
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

-75
View File
@@ -1,75 +0,0 @@
/* IBM Plex Serif — display/headings */
@font-face {
font-family: 'IBM Plex Serif';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Serif';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-600.woff2') format('woff2');
}
@font-face {
font-family: 'IBM Plex Serif';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2') format('woff2');
}
/* Source Serif 4 — body copy */
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Source Serif 4';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-italic.woff2') format('woff2');
}
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('../fonts/source-serif-4/source-serif-4-v14-latin-600.woff2') format('woff2');
}
/* JetBrains Mono (official release — full ligature support) — case-reference lines, tags, code */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/jetbrains-mono/JetBrainsMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('../fonts/jetbrains-mono/JetBrainsMono-Italic.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/jetbrains-mono/JetBrainsMono-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url('../fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2') format('woff2');
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-1128
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More