Part of the jedee wiki — pages written by an AI (Claude Code), supervised by Johan.
jedee wiki
The private wiki documenting web techniques and how jedee is designed and built. Maintained by Claude Code; conventions live in the “LLM wiki” section of AGENTS.md at the repo root. Pages are named after their concept where one exists (“Microformats”, “Text wrapping”): general explanation first, then an “In jedee” section. Purely project-internal pages keep descriptive titles and live under “The jedee site itself”.
Web standards & markup
- Microformats — the mf2 vocabulary that makes HTML machine-readable; in jedee: the
e-contentwrapper, the Stack/owl spacing collision, and why an mf2 class must never be “cleaned up”. - One JSON-LD envelope for sixteen types — ⚠ the schema include builds its path from front matter, so a
schema:with no template fails the build. That constraint is why fifteen types sayBlogPosting. - Per-type feeds — twelve types have one, four deliberately don’t; ⚠ a feed only ever sees the post body, never front matter.
- The lang attribute — marking foreign-language passages with
lang="sv"(page default + per-part); in jedee the homepage greeting, one all-Swedish note, and the mostly-Swedish activities, whose titles are language-detected at build time.
CSS & design
- Text wrapping —
text-wrap: balanceandpretty, the end of JavaScript widow-fixes; ⚠ the globalprettyreset overrides an explicitwhite-space: nowrap(the breadcrumb fight). - The theme toggle — one SVG sun masked into a moon, the reduced-motion split, and the inline script that prevents a flash of the wrong theme.
- Focus rings and paint containment — ⚠ an outset focus ring is invisible inside anything that clips paint. A silent accessibility failure.
- The main menu — a disclosure whose button is injected from a template, so the CSS detects its absence structurally; ⚠ flexbox splits shrinkage by base size, so a wide
flex-basis: autoneighbour starves the breadcrumb. - Choreographing CSS animations — sequencing a multi-step animation in CSS alone: named step times instead of hand-added delays,
clip-pathwipes for proportional fonts, and a caret that tracks the wipe edge with a percentage offset. ⚠ a percentage offset on an absolutely positioned child resolves against the PADDING box, so the caret’s own width eats the gap you opened with padding. - Scroll-aware CSS during view transitions — tested finding:
scroll-state()and scroll-driven animations both reach the::view-transitionpseudos. Not shipped. - Undefined custom properties — a
var()pointing at nothing makes the propertyunset, so it inherits or takes its initial value depending on the property; ⚠border-widthfalls tomedium(3px), which shipped a border nobody chose on every button. - Configuring a layout composition — configure a composition through its published custom properties rather than redeclaring its properties from a block; ⚠ a knob inherits into nested instances of the same composition, and loses outright to a hard declaration on the same element, so every writer has to convert at once.
- Layout breakouts — a named-column grid that lets an element step wider than the prose column; ⚠ the breakout classes only work on a direct child of the grid, so any wrapper in between silently reverts them to content width.
- Tooltips — the three kinds (
title, CSS-generated, scripted) and when each is honest; ⚠ generated content is not a dependable accessible name, so a CSS tooltip must never be a control’s only label, and a::afterinherits from a control styled deliberately unlike body text. - Design token sync — generating a design tool’s tokens from the code that owns them; ⚠ import replaces a set rather than merging, so a token created by hand inside a generated set is deleted on the next import, and
ch/clamp()/color-mix()have no equivalent to export.
Images & media
- Alt text — informative versus decorative, and why
alt=""is an answer rather than an omission; ⚠ an empty alt serializes as a barealt, so grepping the HTML reads it as missing. - Self-hosting remote images at build time — remote covers are pulled in at build; ⚠ the
{% image %}shortcodes break on a remote URL, only a plain<img>works. - Hosting large originals off-repo — the deliberate inverse: 157 MB scans stay in a Cloudflare R2 bucket the build never touches.
- The PhotoSwipe lightbox — the
{% lightbox %}shortcode, deferred behindis-land, degrading to a plain link without JS. - The YouTube embed — a facade with a build-time self-hosted poster and a placeholder that the thumbnail fades over.
- The place map — a Leaflet component that upgrades server-rendered location data into a live map, with three modes chosen by the slotted markup: single pin, place groups, and a recorded GPS route line inlined as GeoJSON, plus a base-layer style switch (themed map / satellite / topographic — the CARTO “streets” style went when CARTO started requiring a key). ⚠ GeoJSON is
[lon,lat], the reverse of Leaflet; ⚠ Leaflet wires attribution removal only for layers added after the attribution control exists.
Publishing & the IndieWeb
- Webmentions — receiving on a static site: hosted endpoint, build-time fetch, self-echo filter, Bridgy backfeed.
- Web Clipper templates — clipping a source page into a frontmatter-complete post; ⚠ three traps (property types, JSON-LD-only, regex triggers) and the vault-ID collision.
- The authoring tool decides the data model — flat front matter where a spec wanted nesting is usually the clipper’s limit, not unfinished work. Find out what writes a field before designing it.
The open web
No pages yet — the home for general material as it’s ingested: personal websites and ownership, copyright and licensing, archiving culture.
Build & delivery
- Three things called cache — the build cache, the browser cache, and the service-worker cache; cache-busting by inlining CSS into the HTML rather than by hashed filenames. ⚠ a long cache on an unhashed URL serves stale until it expires, and changing the header can’t fix copies already cached.
- The service worker's three strategies — network-first pages, cache-first assets, and a cache that wipes itself on every deploy. ~25 lines, no Workbox.
- Progressive web apps — what makes a site installable (HTTPS, a manifest, a service worker) and how little changes when someone installs it. ⚠ most of the manifest layer is Eleventy Excellent stock, not jedee’s own; ⚠
start_urlis resolved at build time, so it is wrong until a rebuild after any domain change. - The accessibility test — the generated pa11y-ci config and the test-only build; ⚠
customPathsis non-empty, so a green run means five pages, in light mode only — and a path that no longer exists scores zero errors rather than failing. - Link checking — scanning source finds things that were never links (code samples, template expressions); ⚠ an all-false-positive report is the same as no report, and this one hid a real one-way-rule breach.
- Watch loops — a build that watches a tree it also writes into will retrigger itself, and so does a second program writing inside the input tree; ⚠ a
.gitignoreline without a leading slash hides nothing insidesrc/from Eleventy’s watcher. - The dev server's memory — telling a JS-heap leak from native memory and from a one-off spike; jedee’s two unrelated OOM crashes, and which one fires when.
- Syntax highlighting — coloring code blocks at build time with Prism; ⚠ a language Prism doesn’t have fails silently into a plain block that looks deliberate, which is what every
njkfence did. - Favicons — the six-file set, and the rule the differences follow from: whether the destination frames the icon for you. ⚠
sharpsToIcoappends its ownresize()to the pipeline you hand it, silently writing the.icoat the wrong size; ⚠ EE’s stock maskable is 612px and transparent, which is the one thing a maskable icon cannot be. - Timestamps without a time zone — a datetime with no offset resolves silently and wrongly; settle it against a spec-defined UTC field or a physical artefact. ⚠ the Strava export column is UTC, and the 2021 backfill read it as local.
- Tailwind — the CUBE boilerplate inversion: Tailwind as a compiler that turns design tokens into
:rootcustom properties, not a utility framework. ⚠ a cascade layer must not be namedutilitieshere — Tailwind 3’s own@layer utilitiesdirective claims the name and hoists the rules into its own bucket. Traced by bisecting the PostCSS pipeline; fixed 2026-09-05 by renaming itcubeUtilities. - Open Graph images — a 1200×630 SVG rendered by Nunjucks and rasterized at build time, committed to the repo because the font only exists on the author’s machine. ⚠ articles are the only post type that gets one; ⚠ nothing ever deletes, so retitles and deleted posts leave orphans.
- Themes and starters — why an Eleventy project is a site and not a theme: a theme layer is a per-file lookup order, and Eleventy has none. ⚠ virtual templates are not a miniature one — a collision with a real file throws, and layouts/includes are excluded outright.
The jedee site itself
- What jedee kept from Eleventy Excellent — the systematic stock-versus-fork inventory: what is untouched, what was extended, what was deleted, and three pieces of machinery left behind with nothing referencing them.
- Anatomy of a post type — the nine places a type has to be wired, and why the layout aliases must never be looped. Start here for the post-type system.
- Permalinks and Obsidian-friendly filenames — Title Case filenames for wikilinks, kebab URLs for the web; ⚠ articles slug from
title, everything else from the filename. - The title-less post types — note plus the five response types: no
p-nameby construction, one shared card, and camelCase keys because Nunjucks reads a hyphen as subtraction. - Wikilinks —
[[bracket]]links, how backlinks are computed data rather than a collection, and why dead links now render as plain text. - The interlinker's second render pass — ⚠ the plugin re-renders every page mid-build, which silently blanks an async shortcode reached through a conditional include. Read before wrapping any include in
{% if %}. - The activities archive — 157 workout posts built backward from a Strava export; frontmatter stores recorded numbers only. Now also how the archive is topped up, and the race-map lightbox.
- Rebuilding an archive from the Wayback Machine — reconstructing a dead service from its dump plus archived crawls, and presenting recovered reactions honestly.
- Site search — a JSON index written at build time, fetched once and filtered in the browser; no search library at a few hundred entries. ⚠
page.rawInputreaches a page’s body at collection time wheretemplateContentthrows, but it is the source before rendering, so template expressions and HTML comments land in the index unless stripped. ⚠ A transitionedvisibilityis stillhiddenon the framefocus()runs, and[hidden]is a UA rule any authordisplayoutranks.
Recent additions
2026-09-06 — enrichment: Watch loops gains a second loop with a different mechanism — not the build writing into its own tree, but Obsidian writing src/.obsidian/workspace.json on every click, with src/ being the vault. Eleventy anchors a bare .gitignore line like .obsidian at the repo root (normalizeIgnoreContent joins and stats it there), so the folder was watched all along and each click was a full 678-page rebuild until the 8 GB heap ran out. ⚠ The symptom looked like images-on-scroll; measured apart, 61 lazy-image requests caused zero rebuilds and one touch of workspace.json caused one. Fix is one watchIgnores line. Source is a new dev note, How the Obsidian watch loop was found. See _log.md.
2026-09-05 — ingest: Tailwind enriched from the two articles it had only been linking to, now clipped into src/_raw/ — Andy Bell’s A CSS project boilerplate and Lene Saile’s What is Tailwind CSS doing here?, with their published: dates filled in. The find is an attribution one that explains the layer bug: Andy’s original global.css uses plain @import with no layer() at all, so the cascade layers — and therefore the utilities name collision — are Lene’s addition in Eleventy Excellent, not in the boilerplate. She also inverted his blocks / compositions order, and jedee predates EE’s later per-token-file prefix mechanism. ⚠ The ingest also proved the scanner finding by accident: saving the two articles took tailwindUtilities from 52 selectors to 55, one of them .md\:text-right, generated purely because Lene’s prose names that class. See _log.md.
2026-09-05 — correction to Tailwind, third and last of the day: most of tailwindUtilities was never a class. The page claimed Tailwind utilities had drifted into the markup, inferred from a list of compiled selectors. Checked against every class="" attribute in the project, 28 of the 52 appear in no class attribute at all — Tailwind’s content scan reads every file under src/ as plain text and keeps any substring that could be a class name, and ordinary words like block, hidden, italic, absolute, table, filter and truncate all qualify. Writing prose about CSS generates CSS; excluding the wiki, _raw and posts from the scan drops the layer from 52 to 42, so these pages inflate the public stylesheet by ten rules. Three more exist only in wiki code samples, including two this very page emitted by quoting class="flex items-center gap-4" as the anti-pattern to avoid. Everything genuinely used is named after a design token, which is the sanctioned use — so the folk description of the setup was right and the drift claim was mine. ⚠ The lesson, in its third form today: measuring the output answers a question about the output; “is this class written in the markup” is a question about the input. See _log.md.
2026-09-05 — correction to the same-day Tailwind ingest, after re-measuring with a PostCSS parse instead of counting braces by hand. The vanished utilities layer is real and now traced: Tailwind 3 carries its own @layer directive (base / components / utilities) from before native cascade layers existed, and cannot tell a genuine @layer utilities { … } apart from its own — so it swallows the block and hoists the rules into its utilities output, which is why compositions and blocks come through untouched. Bisecting the pipeline pins it exactly: after the two import plugins the layer holds 9 rules and tailwindUtilities is empty; adding tailwindcss makes it vanish and tailwindUtilities jump to 61; autoprefixer and cssnano change nothing. Renaming to layer(cubeUtilities) restores it — 9 rules back, tailwindUtilities down to 52, the same nine moving home. ⚠ The page’s second claim was wrong and is retracted: Tailwind’s responsive variants do not escape the layers. A PostCSS parse finds zero top-level nodes outside a layer; the trailing @media sits inside tailwindUtilities. The brace-matching that produced it mis-read the escaped \: in .md\:text-right, which is a good argument for parsing CSS with a parser when the claim is about structure. See _log.md.
2026-09-05 — ingest: Tailwind and Open Graph images, two new pages, written from the code rather than from Lene’s demo articles that currently cover these subjects in her voice — the first step of moving dev content off the post feed. Tailwind: the CUBE boilerplate inversion, where a config plugin walks the resolved theme and emits every token as a :root custom property via addComponents, and the markup is meant to carry no utility classes. Three measurements against the compiled stylesheet complicate the folklore. tailwindComponents is 3.0 KB with zero class selectors, which is the arrangement working exactly as intended. But tailwindUtilities holds ~58 classes, and real Tailwind spacing utilities are in templates (.mt-xl, .my-s-m, .p-s-m) next to the intended gutter-* family. ⚠ The utilities layer is absent from the output entirely — @import-glob … layer(utilities) produces no @layer utilities, and the hand-written utilities compile inside tailwindUtilities after Tailwind’s own, which inverts their intended precedence; harmless today only because the single overlapping class (.grayscale) is declared identically on both sides. ⚠ And Tailwind’s responsive variants are emitted unlayered, so a md: utility outranks every layer. Open Graph images: a 1200×630 SVG rendered by Nunjucks with text layout by lookup table (splitlines(22) plus a chain of {% if %}s picking a vertical start), rasterized by eleventy-img, and committed because the SVG names a font a build server does not have. ⚠ Both the generator and the head reference are article-scoped, and articles is the only type whose data file sets layout: post — so fifteen other post types share one static fallback. ⚠ The !existsSync guard only ever adds: 17 JPEGs against 13 articles, seven of them for EE demo posts deleted long ago. Earned links to Configuring a layout composition, Undefined custom properties, Design token sync and Watch loops from the first; Anatomy of a post type, One JSON-LD envelope for sixteen types and Favicons from the second. See _log.md.
2026-09-05 — ingest: Themes and starters, a new page, from a session that started as a vocabulary question — what to call jedee, given that the same work on WordPress would be called a theme. The answer is site (or site repo), because a theme is only nameable where presentation is separable, and Eleventy separates nothing. The load-bearing part is measured rather than recalled: a theme layer is a per-file lookup order (Hugo checks the project’s layouts/ before the theme’s; WordPress does it with the template hierarchy), and Eleventy has no such thing — dir.includes and dir.layouts normalize to exactly one path each, with no array form. ⚠ Virtual templates are not a miniature lookup order, which is what a first pass in the session claimed: read against 3.1.6, EleventyFiles.js concatenates virtual paths onto real ones and throws on a duplicate (“A virtual template had the same path as a file on the file system”), and isTemplateFile() filters out anything under layouts or includes before that check even runs — so a plugin can add pages, never replace or underlay one. The trade against Hugo is recorded honestly in both directions: a forked starter upgrades by merge, which is noisy but announces itself, where a Hugo override goes stale in silence. Earned links to What jedee kept from Eleventy Excellent (the fork inventory is the only record of the seam) and Watch loops (generated CSS and JS living in src/_includes/ is EE’s choice, and the precondition for that loop). See _log.md.
2026-09-04 — ingest: no new page; the browser-mockup group, and the last of the visuals plan’s named tiers. Ten figures across The main menu (the panel open, the no-JS pill row at 1280 and 375, and the flexbox trap with the breadcrumb crushed to a letter), Site search (the panel open on six real hits, the top two body-less), The YouTube embed (the poster fade frozen at 0, 175 and 350ms), The place map (the orienteering start triangle and finish circles, and the three modes side by side) and The PhotoSwipe lightbox (the clickable strip, and the overlay). ⚠ The keeper is that a fixed-width <div> cannot stand in for a viewport: every spacing and type token is a vw clamp, so a 375px box inside the shooter’s 1400px viewport renders 1400px-scale type — only an <iframe> has its own viewport, and this is the first form of the accidental-variable trap that would have looked entirely plausible in the output. The mockups now run the site’s own JavaScript as well as its stylesheets — the map and lightbox shots are the shipped esbuild bundles rendering the real custom elements (⚠ as classic scripts: a fetched module script is blocked from file:// as cross-origin), and Leaflet’s tiles load fine over file:// because a tile is an ordinary <img>. mockups:check now walks every frame and screenshots element handles, since page.$$eval never saw inside an iframe at all. Three stale claims the pictures caught, all of them prose rather than shot: the no-JS menu wraps to three rows at 1280px and not two, the route line is the accent orange and not a blue standing in for orienteering purple, and route mode opens on Topographic rather than the themed default. See _log.md.
2026-09-04 — ingest: Progressive web apps, a new page, from Max Böck’s 2017 “How to turn your website into a PWA” plus a fresh read of the shipped code. It carries the manifest and installability half and defers the caching to The service worker's three strategies and the icon surfaces to Favicons. The find worth having is an attribution correction: the manifest template, its <link rel="manifest"> and both <meta name="theme-color"> tags are Eleventy Excellent stock — checked against upstream, where the only difference is that EE appends the site description to name — so an unmodified EE site already ships a manifest and is one service worker away from installable. Only the worker and its registration are jedee’s own, and that worker is Böck’s from eleventastic. ⚠ start_url is meta.url, resolved at build time, so it is stale after any domain or environment change until a rebuild. Its own mockup is the one thing Favicons’ six frames do not show: the same page in a browser tab beside the same page installed, where the two bands of browser furniture are the whole of what display: standalone gives back — and both status bars are deliberately the same color, because the browser tints its own from the same meta tag and a browser-gray one would have invented a difference. Age-noted on the page: sw-precache is retired in favour of Workbox, the Lighthouse extension is now a DevTools panel, and “a better Google ranking” is a 2017 argument. See _log.md.
2026-09-04 — ingest: no new page; the two filmstrips. Choreographing CSS animations gets six frames of the start page typing its own name, with the caret visibly riding the wipe edge; The theme toggle gets the sun-to-moon morph at five instants — the middle frame a plain disc, which is the page’s “there is no moon shape” made visible — plus its contrast table redrawn as four suns on the light page, where the two that fail 3:1 are visibly barely there. Both are made with the technique the animations page itself documents: every animation in each frame paused and seeked to a different currentTime, so nothing is running when the shot is taken. Both shooters now wait on a window.__mockupReady flag, so a mockup that freezes something can say when it has settled. ⚠ A transition only exists where a value changes — the morph’s first version rendered each frame already dark, so no CSSTransition was ever created and all five came out as the finished moon (measured with getComputedStyle, not spotted by eye); the frames have to be built light, flipped, and then frozen. ⚠ And a gate that includes anything differing by construction is not a gate: data-compare sat on the cell, which also holds a per-frame caption, so it would have gone green on five identical glyphs — moved onto the glyph alone it failed at once and found that bug. See _log.md.
2026-09-04 — ingest: no new page; the plan’s three comparisons landed — Undefined custom properties (five button variants at the accidental 3px above the same five at 1px, so the asymmetry the page argues for is visible: four variants absorb it into a border nearly their own background, the ghost button does not), Focus rings and paint containment (the same focused play button with the ring clipped away and with it inset at -4px, the left half deliberately a picture of an absence), and Syntax highlighting (one njk fence flat beside the same fence as jinja2). ⚠ npm run mockups:check had to stop reading words: it compared where each side’s line breaks fell, which is no evidence at all when the variable is a border width, a focus ring or a color — it now hashes a screenshot of each [data-compare] element and fails on pixel-identical sides, the more general form of the same claim, proven by forcing it red. ⚠ The accidental-variable trap returned in a new disguise: the two syntax panels wrapped differently because code.css sizes code as well as pre, so a size set on the <pre> never reached the text — caught by measuring the rendered boxes (513.8px vs 514.1px once pinned), not by looking. Two things worth reusing: a mockup can link the site’s compiled global.css rather than copy token values, so the shot cannot drift from the stylesheet it depicts; and drawing the real component measured a small correction to a page’s own claim, since lite-yt-embed’s :focus { filter: none } does un-gray the play button — not a focus indicator, but not nothing either, and the page now says so. See _log.md.
2026-09-04 — ingest: no new page; Text wrapping enriched with two measured specimens — one heading set greedy beside balance, one paragraph set greedy beside pretty — and the device-frame routine generalized so any page can have one. shoot-device-frames.js became shoot-mockups.js (npm run mockups): it walks every _sources/*.html and shoots every [data-shot] element to the filename that attribute names, so a new visual is one HTML file and no script edit; it is less code than before, and it reproduced all six device frames byte for byte. The CSS opt-out widened with it, data-wiki-devices → data-wiki-mockup, since a specimen card has its own transparent rounded corners for the same reason a phone does. ⚠ The load-bearing lesson is that a side-by-side comparison proves nothing unless both columns are exactly the same width. The first specimen hung the divider off the second column as padding, making it 24px narrower, and produced a confident difference in every pair that was purely the narrower measure — equalized, both pairs rendered identically. npm run mockups:check now measures where the line breaks actually fell (a rendered line is not an element, so each word’s top edge is the evidence) and fails when two sides match; it was proven by forcing it red, not by trusting a green run, the same trap as The accessibility test. ⚠ Also measured: text-wrap: pretty is far more conservative than its reputation — on the paragraph first drafted for it, it changed nothing at 21 of 23 widths — so a demonstration paragraph has to be chosen for it, and both specimen widths are swept rather than picked. Survey of the rest of the wiki in _local/design/Plan - Wiki visuals.md. See _log.md.
2026-09-04 — ingest: no new page; Favicons enriched with the set rendered onto a device — the six places an installed app shows it (home screen, install prompt, splash, running light and dark, the app-switcher card), as six figures whose captions are real text rather than pixels. The keeper is that the frames are regenerable: the preview page they come from now lives in the wiki’s own repo at _sources/pwa-preview.html, points at the committed icons rather than copies, and npm run mockups re-shoots the PNGs from it — run against a clean tree it reproduced them byte for byte. Three traps, none of which fails anything: prose’s image rule (border: var(--stroke) plus a radius) traces a transparent-cornered PNG’s bounding box, drawing a rectangle around the rounded device, so the frames opt out via a data-wiki-mockup exception rather than a change to the site-wide rule; declaring an image’s display size makes eleventy-img serve that size, so a 560px source declared at 280 shipped soft on retina and the attributes have to carry the true intrinsic size; and a mockup’s own colors are only valid against the ground it was drawn on — the phone bezel at #1c1f24 vanished against the dark theme’s #141619 page, the same shape as Undefined custom properties’ surface lesson. Also filed: .popout reaches markdown content through .wrapper-pass’s subgrid, so the grid breaks out with no new CSS (Layout breakouts). See _log.md.
2026-09-03 — ingest: Favicons, from the session that regenerated the whole icon set in the brand orange. The general half is the rule that makes the set make sense — a browser tab does not frame the icon, so it gets transparency and nearly the whole box, while every OS surface frames it and can afford an inset. Three consequences that are easy to get backwards: apple-touch-icon must be opaque and must not carry baked corners (iOS masks it again), and a maskable icon must be opaque and full-bleed or the wallpaper shows through the launcher’s crop. Also why theme_color feels invisible — <meta name="theme-color"> overrides the manifest the moment a page loads, leaving the manifest value only the splash and the app-switcher card. The load-bearing find is sharpsToIco: it appends its own resize() to whatever sharp pipeline it is given, and since sharp resizes before extending regardless of call order, resize(28).extend(2) became resize(32).extend(2) and wrote a 36px .ico with no error. EE’s stock script is immune because nothing is pending — the trap only appears once padding is added. The same pass found EE’s maskable at 612px against a manifest declaring 512, and transparent. Source is a new dev note, How the favicon set is generated. See _log.md.
2026-09-03 — ingest: Site search enriched with “Opening it, and clearing it”, from the session polishing the panel. Two traps that both look like JavaScript failing: a transitioned visibility is still hidden on the frame the transition starts, so focus() on open is a silent no-op — hidden by the fact that reopening within the closing fade works, so only the session’s first open fails; and [hidden] is a UA rule of display: none, which any author display beats at equal specificity, so a class setting display has to restate it or its hidden attribute is decoration. Plus why ::-webkit-search-cancel-button needs a rule to itself — Firefox does not know the pseudo-element, and an unknown one invalidates every selector grouped with it. See _log.md.
2026-09-02 — ingest: Site search, from the session that built the header’s search panel. The general half is choosing between a hosted service, a search library and a plain Array.filter over a JSON file — at a few hundred entries the last one is not a compromise, and Ariel Salminen’s write-up (2025) is the Eleventy shape it takes; her regex chains turn out to be a symptom of assembling JSON inside a Nunjucks template, which JSON.stringify removes the need for. The load-bearing find is page.rawInput: it is populated on collection items, so a page’s body is reachable where templateContent still throws “Tried to use templateContent too early” — and because it is the source before rendering, {{ personal.address }} and <!-- TODO: … --> comments shipped into the index before being stripped. Also the two-panels-one-anchor problem it shares with The main menu, why the panel redeclares its surface instead of reading --megamenu-surface (Undefined custom properties), and a contrast trap worth its own line: a color-mix() that clears 4.5:1 on the panel can fail on the highlighted row, whose own tint is a second surface. Source is the clip, Building Search Index with Eleventy. See _log.md.
2026-09-02 — ingest: Tooltips, from the session that gave the header’s two icon-only controls a label on hover and keyboard focus. The page leads with the decision the rest follows from: content: attr(…) is not a dependable accessible name, so a CSS tooltip has to be redundant with a real visually-hidden label — and the moment it isn’t, it needs the scripted aria-describedby treatment instead. Also the :hover-only trigger that leaves keyboard users with nothing, the page-edge overflow a centred tooltip causes (on every page, since a visibility: hidden element still takes part in layout), and why visibility has to be transitioned alongside opacity. Undefined custom properties enriched with the inherited-branch twin of its own bug: font-weight: var(--font-normal) — a token that has never existed — fell to unset and took .menu-toggle’s 700, which refines that page’s “the inherit branch looks fine” into “…only when the parent is ordinary body text”. The theme toggle gained a short section on its label and the title it dropped. Source is a new dev note, How the header tooltips work. See _log.md.
2026-08-29 — ingest: no new page; The PhotoSwipe lightbox enriched by the session that gave jam album covers a lightbox, and corrected where it claimed activity covers were the only cover-bearing layout that could have one. That constraint is the {% lightbox %} shortcode’s ./src prepend, not the lightbox’s — a layout can invoke the component directly with any URL, which is what the photo pages had been doing all along. New: reading the zoom target’s dimensions at build time (PhotoSwipe needs them up front, and the read pays for self-hosting the file), why it runs as computed data rather than an async filter Nunjucks cannot await inside {% if %}, and a silent trap — WebC drops a slotted node carrying eleventy:-namespaced attributes, rendering an empty link with no warning. Source is a new dev note, How jam covers reach the lightbox. See _log.md.
2026-08-28 — ingest: Choreographing CSS animations, from the session that built the start page’s wordmark reveal (the name types in behind a caret, blinks twice, backspaces away). Covers the delay chain expressed as named step times, why a steps() typewriter needs monospace and a clip-path wipe doesn’t, and the percentage offset that lets a caret track text of unknown width. Two things measured rather than assumed: a percentage offset on an absolutely positioned child resolves against the padding box, so the caret’s own width swallowed all but 0.45px of an intended 3.37px gap; and the animation clock starts when the element is first styled — 12 ms in on this build, 60 ms before first paint — so on a slow connection a visitor lands mid-animation. Also the rule that keeps a reduced-motion visitor whole: author the base state as the resting state and let the animation opt in. Source is a new dev note, How the start page types its own name. See _log.md.
2026-08-26 — ingest: no new page; The YouTube embed enriched with an oEmbed pre-commit check for video slugs. A removed video doesn’t fail the build (the poster filter’s catch degrades it) but leaves a broken embed, so verify a slug against youtube.com/oembed before committing — 200 with title/author_name means live and embeddable, 404 means gone, and author_name also flags fan re-uploads over official channels. The rest of the jams session (front-matter normalizing, filling videos, two artist/title fixes) was archive curation and stayed in LOG. See _log.md.
2026-08-23 — ingest: Layout breakouts, written from the composition itself after a lint pass found The place map linking to a page that had never been written. Covers Ryan Mulligan’s named-column grid (Eleventy Excellent stock, credited in the file) and the constraint that makes it awkward: grid-column belongs to a grid item, so a breakout class inside any wrapper silently does nothing. jedee answers that four different ways depending on who owns the wrapper — the .wrapper-pass subgrid for post bodies (jedee’s own, @supports-guarded, with :where() keeping the fallback at zero specificity), a plain <div class="popout">, the image shortcode’s containerClass, and <place-map>'s @breakout prop. Source is the CSS. See _log.md.
2026-08-23 — ingest: Syntax highlighting, from the session that found every njk block on the wiki rendering flat. Prism has no Nunjucks grammar, and an unknown language falls back to a default that emits no tokens — indistinguishable from a block deliberately left bare. Fixed by rewriting the fence’s language to jinja2 before Prism sees it, rather than aliasing the grammar under the name njk: Prism’s markup-templating hook is keyed on the language name, so the alias highlights the tags but leaves the surrounding HTML as token soup. Source is a new dev note, How njk code blocks got syntax highlighting. See _log.md.
2026-08-23 — ingest: Configuring a layout composition, from the session that stopped main-nav.css rebuilding the cluster composition by hand and gave repel the wrap knob cluster already had. Two traps, both hit live rather than read about: a custom property used as a config knob inherits, so pinning the header cluster silently un-wrapped the pill row nested inside it — @property { inherits: false } closes this, tested directly — and a knob loses to a hard declaration on the same element under CUBE’s layer order, so converting one writer without the other is inert. The main menu corrected: its “flexbox trap next to it” section quoted the pre-refactor CSS. Source is a new dev note, How the header row's wrapping got a knob. See _log.md.
2026-08-22 — ingest: no new page; Design token sync enriched by the session that gave border width and the two shadow recipes a token source. Three findings: Penpot’s plugin API cannot create a shadow token at all, rejecting every shape including the one Penpot itself stores, though the UI and the DTCG import both manage it fine; the way out is to make one by hand and read it back, since the API can read what it cannot write; and an import rewrites which token sets are active, so the canvas can change theme underneath you. Also corrected two stale claims on the page — seven sets are emitted now, not six, and core/button is no longer safe-by-accident. Source is a new dev note, How the shadow tokens got into Penpot. See _log.md.
2026-08-22 — ingest: Design token sync and Undefined custom properties, from the Penpot/jedee audit session. Two silent failures: a hand-made token inside a generated set is destroyed on the next import, and a var() with no definition falls to unset rather than being skipped. Sources are two new dev notes. See _log.md.
2026-08-15 — ingest: no new page; two corrected. The place map loses its Groups mode — /activities/ became one chronological index, so the grouping, its toggles and the map chips came out of the component, replaced by a flat Places mode; kept the two lessons underneath (reserve the map box server-side or the page drops 16:9-of-the-column on idle, and watch for a double reverse when a filter re-reverses an already-reversed collection). The accessibility test corrected on a point it had got wrong three sessions running: the recurring “Could not find Chrome” is not a stale cache to re-download but puppeteer arriving transitively under pa11y-ci, pinning an exact build, and swallowing its own failed download — now pointed at an installed Chrome via meta.tests.pa11y.chromePath. Source is this session; see LOG 2026-08-15 and _log.md.
2026-08-11 — ingest: The place map enriched with its base-layer style switch, from the session that added it — a satellite / topographic / streets picker beside the themed default. Two Leaflet findings recorded: attribution removal is wired only for layers added via layeradd, so a layer added before the attribution control keeps its credit forever; and corner controls stack last-added-nearest-the-edge. Source is a new dev note, How the place map switches tile styles. See _log.md.
2026-08-11 — ingest: The place map, from the session that drew recorded GPS route lines on the activity pages — the third mode of the Leaflet map component, alongside its single-pin and place-list modes. Three things called cache corrected: the component scripts no longer share the fonts’ year-long cache — an unhashed URL under a long max-age served stale for months, and the page’s old “must-revalidate covers it” reasoning was wrong about when. Source is _local/design/Plan - GPX route line on the activity map.md. See _log.md.
2026-08-10 — ingest: two pages extended by the session that gave the activities archive a feed. Per-type feeds — activities was the fourth feed-less type with no reason recorded, and now has one; the page’s own “a feed only sees the body” wrinkle is why it needed a sixth shared body. Self-hosting remote images at build time gained the transform’s per-image eleventy: attributes: the global widths: ['auto'] means “the original”, so a plain <img> ships a 4032 px race-map scan, and eleventy:widths caps one image without touching what the page asks for. See _log.md.
2026-08-09 — ingest: Timestamps without a time zone, from the session that imported the 2026 O-Ringen stages and found the Strava export’s date column is UTC, not local. The activities archive extended with how the archive is topped up and the race-map lightbox; The PhotoSwipe lightbox gained its second automatic surface and the wrapper-width finding. Source is a new dev note, How activity posts are imported from a Strava export. See _log.md.
2026-08-08 — ingest: Alt text, from the session that gave the cover image on five post types a describable coverAlt. The accessibility test corrected — its path list had said four pages since 2026-08-05, when a fifth was added — and given the two failure modes that session hit. Source is a new dev note, How cover images got real alt text. See _log.md.
2026-08-03 — ingest: The main menu, from the session that made the nav usable with JavaScript off. Source is a new dev note, How the main menu degrades without JavaScript. See _log.md.
2026-08-03 — ingest: Link checking, from the session that took the interlinker’s dead-link report from 13 warnings (all false) to 0. Source is a new dev note, How the dead-link report was made usable. Wikilinks corrected in two places it had got wrong. See _log.md.
2026-08-03 — structure: the general-first shape brought to the 17 remaining pages from the July seed ingests. Each now opens with the concept and moves the site-specific material under an “In jedee” heading; the six purely project-internal pages are exempt by convention. Titles deliberately unchanged. See _log.md.
2026-08-02 — ingest: Watch loops and The dev server's memory, from the session that fixed a dev server dying on a single CSS edit. Two causes, one ours and one Eleventy’s. Source is a new dev note, How the dev server was made survivable. See _log.md.
2026-08-01 — ingest: The lang attribute, from the session that marked the site’s Swedish text for accessibility. Source is a new dev note, How Swedish text is marked with lang. See _log.md.
2026-08-01 — structure: concept-titled pages (general first, “In jedee” second), no person pages, this index reorganized. Microformats converted as the model page; Text wrapping is the first external ingest (Josh Crain’s widows post). See _log.md.
2026-07-31 — first lint pass. Six factual corrections against live code, one new page (The authoring tool decides the data model), and the Web Clipper page is no longer an orphan. See _log.md.
2026-07-31 — third ingest, Lene’s upstream EE docs. Two pages: the stock-versus-fork comparison and the pa11y workflow. Seeding complete.
2026-07-31 — second ingest, _local/project_docs/. Five cross-cutting post-type pages plus the two caching pages; the interlinker, cover-image and microformats pages enriched in place. See _log.md.
2026-07-31 — first ingest. Fourteen pages from the thirteen dev notes (now retired to src/_raw/dev-notes/).