2026-08-18

  • Added Bacon (No. 135) to Sides — oven bacon started in water, written for BLTs. 400°F on a rimmed half-sheet with ~120g of water: the pan holds at ~212°F until the water boils off, so the fat renders before the surface browns, which is what keeps the strips flat and stops the thin edges scorching. 22–30 min standard cut, 30–35 thick. Notes carry the sandwich specifics (halve the strips crosswise before baking; toast the bread in the reserved fat) and the two failure modes — a crowded pan steams, and a rack costs the even browning that frying in the rendered fat gives. No hero photo; nothing in the registry depicts it.

2026-08-11

  • Added Tart Shell (No. 131) to Pantry — Dominique Ansel’s go-to sweet shell. Sourced to his “go-to vanilla sablé” from Everyone Can Bake (2020) via The Splendid Table and the Food52 reproduction, which agree on quantities and the 350°F / 15–20 min blind-bake: 185g flour, 85g confectioners’ sugar, 50g cornstarch, 130g butter, 1 egg, salt, vanilla. Verifying the source corrected the brief: the recipe was requested as an almond-flour pâte sucrée, but Ansel’s canonical shell uses cornstarch, not almond flour, and is rubbed rather than creamed (a sablé) — no reputable source reproduces an almond-flour version, so the cornstarch sablé is what shipped.
    • Filed in Pantry beside pâte brisée as its sweet counterpart, and the lede/notes/Analysis are written as an explicit contrast to it. The Analysis (46.6% structure, 22.2% fat, 16.9% sugar, 11.7% liquid) shows the difference from brisée (55.4% / 24.4% / 0 / 18.0%) is not the fat — it’s the added sugar, the cornstarch (10% of the dough, pure starch with no gluten protein), and the absence of any free water, which together is why it bakes short and sandy rather than flaky. This also settles the open question left in brisée’s own note (“pâte sucrée is not this dough with sugar added”).
  • Added Lemon Tart (No. 132) to Sweets — Marco Pierre White’s tarte au citron from Harveys, in the tart shell above. Filling quantities (9 eggs, 400g caster sugar, zest of 2 lemons, juice of 5 ≈ 250g, 250g double cream, 250°F bake) confirmed exactly by two independent reproductions, The Caterer and BigSpud; a third source is a scaled-down variant and was not blended in. MPW’s own oversized 500g-flour pastry was dropped in favor of composing the classic filling into the tart shell, the same sub-recipe pattern Flan Pâtissier uses with brisée.
    • The Analysis covers the filling (per the composite-dessert rule) and explains the signature 250°F bake: at 4.4% egg protein and zero starch, it is a naked egg custard that curdles above ~185°F, so unlike the starch-shielded flan (which bakes hot) it must bake low. The 30.4% sugar (vs flan’s 13%) and the lemon acid both modify the set — sugar raises and slows coagulation, acid firms it — which is why a starchless citrus custard slices cleanly. Cross-linked to purin (pure egg-set) and crème brûlée (yolk-cream) as the set’s neighbors.
    • Kept MPW’s classic quantity honest about capacity: the ~1350g of filling is sized for a deep 8-inch tin (his is 3.75cm) or a 9-inch, so the recipe says to line the shell walls tall or bake the excess in ramekins rather than silently scaling the famous ratio down.
    • No hero photos for either: nothing in the registry verifiably depicts these dishes, and there is no fallback by design.
  • Promoted Pineapple Cake Tart out of Drafts into Sweets (No. 133). The draft (added 2026-08-09) was complete — full ingredients, steps, a dual Analysis block for shell and filling, and notes — so promotion was a status change, not a rewrite: moved drafts/r/, dropped the /drafts/ permalink for the sequential number, removed it from drafts.md, and linked it under Sweets. No hero photo — a plain “pineapple tart” image would show the small Nyonya cookies, not this open 9-inch format, so none was added.
  • Added Lemon Curd Tart (No. 134) to Sweets — a thin, stovetop-cooked lemon curd baked briefly in the tart shell. The classic tarte au citron form, deliberately set as the counterpart to the lemon tart (No. 132) directly above it: where that pours a raw cream custard into a deep shell and bakes it low and long, this cooks the curd to a nappe (~170°F) on the stovetop first, enriches it with butter off the heat, then gives it only a 10–15 min set-bake in a thin blind-baked shell. Quantities from the two classic baked-curd sources, which agree on the ratio (~4 eggs : 200–225g sugar : 150–240g juice : zest of 3–4 lemons : 115–170g butter): Smitten Kitchen (5 eggs, 225g sugar, ⅔ cup juice, 115g butter, 15-min set-bake) and Fat Daddio’s (4 eggs + 3 yolks, 1 cup sugar, 1 cup juice, ¾ cup butter, 5–8 min bake); scaled to a clean 500g batch (3 eggs + 1 yolk, 130g sugar, 110g juice, 90g butter) that lines the tart shell as a thin layer.
    • The Analysis contrasts it with No. 132 on the one number that separates them: 19.0% fat here vs. 12.2% there, almost all of it added butter. That tart sets a raw filling by egg protein alone, so it must bake low and long; this pre-cooks the curd on the stove (sugar at 27.8% and lemon acid both holding the eggs back from scrambling at 170°F), so it goes in thin and the oven only firms the surface. Cross-links to the Pierre Hermé crème au citron as the richer no-bake variant (roughly double the butter, blended and chilled, never baked) — noted but not the recipe, since the brief was explicitly thin and baked.
    • No meringue by design (that would make it a lemon meringue pie); finished bare or with confectioners’ sugar / candied zest. No hero photo — nothing in the registry verifiably depicts this dish, and there is no fallback.

2026-08-09

  • Added Pineapple Cake Tart to Drafts — a Taiwanese pineapple cake (鳳梨酥) crust and filling in the open format of a Nyonya pineapple tart. (Promoted to a published recipe on 2026-08-11 — link updated to its new location.) Untested, so it goes to Drafts with no number. Searched for prior art first and found none: recipes exist for the enclosed cake in a mold, for open-faced Nyonya tarts with a plain butter pastry, and for American pineapple shortbread bars, but nothing that puts the milk-powder shortbread and the reduced-pineapple paste together in a tart shell. So the two halves are lifted from published recipes and only the format is invented.
    • Filling anchored on Sift & Simmer (750g pineapple + 454g candied winter melon, 325°F) and 196 flavors (680g pineapple, 250g sugar, 20g filling per cake, 340°F); crust on Jaja Bakes (cake flour + milk powder + almond flour, 2:1 dough to filling at 30g/15g, 320°F) and Oh My Food (30g dough / 22g filling, 320°F); the open-faced format on MyKitchen101 (~9g dough to ~8g jam, 355°F) and What To Cook Today (10–15g dough per tart, 350°F).
    • The recipe’s real contribution is a number the pineapple-cake sources leave as an adjective: pull the filling at 300g from 1000g of flesh. They describe the endpoint instead of weighing it — “very thick and dry paste”, “dark yellow/golden”. Two jam recipes do publish a yield, and both are looser than this: Bakeomaniac 350g from 850g whole fruit (~1.4:1 on flesh) and What To Cook Today “440–500 grams” from 2kg untrimmed (~2.5:1). A paste that holds a cut edge in an open shell goes further — 3.3:1. At 320g the filling is 21% water and weeps into the shell; at 280g it sets hard enough to crack at the cut.
    • Opening the format inverts the dough-to-filling ratio, so the sugar had to come down. A pineapple cake is ~2:1 dough to filling (30g around 15g); this tart is 315g to 300g, about 1:1, so every bite carries twice the paste. Sources add 200–370g of sugar per kg of pineapple; this adds 70, and the finished paste still lands at 64% sugar because the fruit brings ~100g of its own.
    • Analysis block covers both components separately (shell and filling), following the precedent set by Tarte Tatin and French Silk Pie. The shell’s 30.5% fat and 7.9% liquid — above pâte brisée’s 24.4% / 18.0% — are why it is pressed rather than rolled, and why it needs no baking weights.

2026-08-03

  • Added Salmon en Papillote (No. 130), written against ten published recipes rather than from memory. Salmon baked over shingled potatoes in a sealed parchment packet. The sources disagree on almost everything — oven 350–450°F, bake 9–25 minutes, potatoes raw or boiled 30 minutes — so the notes give the number and the spread, and say which variable actually explains it (fillet thickness, not weight). Anchors: Epicurious (Gourmet; 3mm potato slices blanched 2 min, 400°F, 20 min), La Cucina Italiana (potatoes boiled 30 min then sliced 6mm, 450°F/15 min, vermouth), Lena’s Kitchen and The Mediterranean Dish (raw mandoline-thin potato, 375–425°F), Well Seasoned Studio (125–130°F pull, 1 Tbsp wine per packet), Fifteen Spatulas (125°F), A Couple Cooks (9 min at 1 inch, 10–11 min thicker), NYT (10–15 min, “should puff up”), and the French home version on Marmiton / CuisineAZ (foil, raw potato, 200°C/25 min).
    • Three details I’d have written from memory are wrong, and the sources are why. A 5-minute par-boil is twice what any source uses for 3mm slices — Epicurious blanches exactly 2 minutes, so the recipe says 2. “Pull at 122–125°F” is below the range the two recipes that state a temperature actually give (125–130°F); the recipe now pulls at 125°F and names the 145°F FDA floor. And “seal it tight” is the opposite of the instruction — The Mediterranean Dish says explicitly “do not fold too tightly, as the steam and the heat need some space to expand,” so the step now says seal fully but leave room to inflate.
    • No potato-under-salmon consensus exists, so the recipe picks and says so. Both French sources put the sliced potato on top of the fish; the English-language ones bed the fish on the potatoes. Bedded won — the potatoes get the butter and the rendered fish fat, and the fish is lifted out of its own liquid. The note also records the one number no source supports at all: nobody publishes a liquid volume per packet except Well Seasoned Studio’s 1 Tbsp, which is where the 15g comes from.
    • No hero photo: nothing verified in the registry depicts this dish, and there is no fallback by design.
    • Fixed a broken link in this changelog that the link checker passes. […](./r/matcha-shortbread) in the 2026-07-27 entry resolved to /recipes/changelog/r/matcha-shortbread — a live 404, verified against the deployed page, because ./ is relative to the permalink /changelog/, not to the file. check-links.py treats ./ anchors as index-relative and so reported it clean; only index.md sits at the root where that holds. Both changelog recipe links now use relative_url.
  • Moved 8 recipes into Starters, which now holds 10. Reviewed all 129 against one test — a starter is served before or apart from the meal, in a small portion; “could be eaten as a snack” doesn’t qualify, since nearly everything could. Eleven were arguable and the calls were made by rendering each affected homepage section rather than arguing in prose. Moved: the three tea sandwiches and Tamago Sando (from Mains — four crustless sandwich recipes at yield 4, and ham-and-cheese’s own note already said “pairs well with the cucumber and watercress sandwiches for a full afternoon tea platter”); Savory Canelé (from Sweets — its lede says outright “served warm as a bite, not a dessert” and its note says “eat warm with an apéritif”, so the category was contradicting the page); Chawanmushi (from Soups — a savoury steamed egg custard in a lidded cup is the classic Japanese first course, and it is not a soup; it landed there because it’s dashi-based and eaten with a spoon); and Tortilla Española with Lazy Tortilla (from Mains — cubed, it’s the definitive Spanish tapa).
    • Tortilla brought its variant with it, which was the one non-obvious consequence. Steering requires the base recipe to be listed before its variants, so moving Tortilla without Lazy Tortilla would have split a pair across two categories and broken that ordering rule. Both moved; base still first.
    • Savory Canelé leaves the 16-recipe canelé run, and that’s a real cost, not a free win. It shares the method and the Analysis block with its siblings, so anyone browsing canelés stops finding it there. Taken anyway because a page whose first line says “not a dessert” has no business filed under Sweets. Its ## Analysis block still renders: nothing in main.scss is scoped per recipe category (zero body.section-sweets rules), so a category change carries no styling risk — verified before moving rather than after.
    • Left alone, with reasons: Gyoza (40–50 dumplings is a sharing quantity, but a plate of gyoza and rice is dinner for most people cooking it), Karaage (izakaya dish, but 600g of chicken thigh is a main-course quantity), Salade Niçoise (classically a first course, but tuna + egg + potato is a complete plate), Ohitashi (cold and small, but it’s squarely what Sides means here, and Sides is already the smallest section at 6 — taking from it to fill Starters is robbing Peter).
    • Section counts: Starters 2 → 10, Mains 22 → 16, Soups 13 → 12, Sweets 51 → 50. The masthead count is unchanged at 129 and auto-computed, so it needed no edit.
  • Corrected the shrimp timing table against published sources — the colossal row was the one number no source supports. The flagged cell was 7–9 minutes off heat for shell-on U-15. Checked against real recipes instead of my own estimate: ATK’s Classic Shrimp Cocktail rests shell-on 16–20 for 5–7 min, ATK’s How to Not Overcook Shrimp gives peeled extra-large-to-jumbo 5 min covered from boiling water, ATK’s Mexican Shrimp Cocktail 5 min, Food Network ~2 min peeled with “a minute or two longer” shell-on, and Li et al. 2020 measured unpeeled prawns needing exactly one more minute than peeled. The anchors converge on 5 minutes for extra-large and jumbo, and the only measured shell penalty is a single minute — not the 2–3 the old colossal row implied. 16–20 shell-on 5–6 → 5–7 (matching the one tested published row), U-15 shell-on 7–9 → 6–8, 21–25 head+shell 5–7 → 5–6.
    • U-15 head + shell now reads “probe it” instead of a time, and that cell is the real finding. No published recipe gives an off-heat time for colossal at all — ThermoWorks abandon the clock for a thermometer at that size. A number I could not source had no business sitting in a table that looks authoritative, so the table says so and gives 140°F at the thickest part instead. A new “where these times come from” table lists every anchor with its link, and states plainly that no authoritative count-per-pound chart exists (every one that turns up in a search is machine-generated) — so the column shape is reasoned from the ATK anchors plus the measured one-minute penalty, not copied from a source that had already done the work.
    • Two other claims of mine were wrong. “Proteins set between 120 and 140°F” was unsourced invention; replaced with what three sources actually converge on — pull at 140°F (ThermoWorks), a 145°F seafood floor (FDA), water held 155–163°F by size. And “the shell is a barrier” implied insulation, which is the popular explanation and not what the literature says: a shrimp cuticle is tens of microns in front of an ~11mm tail, a fraction of a percent of the meat behind it, and nobody has published a thermal conductivity for intact shrimp shell — a 2026 review calls that a genuine gap. Li et al. credit added heating distance plus the air gap between shell and muscle, and less moisture lost; McGee’s reason is flavour leaching, not heat. A new section states the mechanism correctly and says the shell buys about a minute, not a wide margin of safety.
    • Size names have no industry standard, and the article now says so with receipts. The American Shrimp Processors Association — “since these appellations are not universally defined and regulated, the numbers system works best” — and Fulton Fish Market — “there are no industry standards for shrimp sizing names.” “Jumbo” spans 13–15 to 26–30 across major sellers, a factor of two in the weight of one shrimp, and the federal grade standards never use the words jumbo or colossal. The matrix rows dropped their trade-name labels and are now bare counts; buy by the number on the bag.
  • Add Shrimp Cocktail (No. 127) and Coctel de Camarón (No. 128) in a new Starters category, plus Cocktail Sauce (No. 129) in Pantry and a Poaching Shrimp article. Both cocktails use the same residual-heat method — boil the court bouillon hard, add the shrimp, kill the heat, cover, and let the pot’s stored heat cook them — so the method and its reasoning live once, in the article, and each recipe carries only its own assembly.
    • The timing table is a matrix, not a list, because the data has two independent variables. Count per pound × shell state (peeled / shell on / head + shell) is 6 × 3, of which 16 cells are real — 32 numbers, which is past the point where a table beats prose. A flat one-row-per-case list carried 5 of those 16 and collapsed both variables into a reading order, which hid the useful part: the shell is worth a flat minute up to 21–25 count and 2–3 minutes at colossal, because past that mass matters more than the barrier. Rows are count, columns are shell state, and the article says to read across.
    • The times are stated as a starting point, not a measurement. They are not instrumented, and the article says so in the same breath as the table — the test is cutting one open at the low end of its range (opaque through, C-shaped not O-shaped). Underdone costs 60 seconds with the lid back on; overdone costs the shrimp.
    • Chosen by rendering, not by arguing. Five geometries for the same 32 numbers were built at the site’s real tokens and real 904px article measure — flat list, plain matrix, semi-graphic with aligned bars, one shared zero-anchored minutes axis, and small multiples — and the plain matrix won on the grounds that it needs no new CSS and is the one you can cook from. The chooser asserted its own geometry from the rendered pixels and caught a real bug in the semi-graphic option: table auto-layout sized each state column to its own content, so one minute measured 11.0px in one column and 17.5px in another — a lie factor of 1.589 between two marks of equal value, which is per-column re-normalisation by accident. Fixed to 1.000 before the option could be judged. That option wasn’t chosen, but a broken render would have made the comparison meaningless.
    • starters is a new category and needed three things, not one. The section in index.md, the label in the breadcrumb replace: chain in _includes/header.html (without it the crumb renders the bare slug), and the valid-values list in steering.md. No stylesheet change: nothing in main.scss is scoped per recipe category, so a new one inherits the recipe treatment.
    • Shell-on shrimp are specified over peeled throughout, because the shells simmered back into the strained poaching liquid are the caldo the Sinaloa cocktail is built on — buying pre-peeled saves ten minutes and costs the broth. The ice bath is salted for the same reason the poaching water is.

2026-07-31

  • Split the pâte brisée out of Flan Pâtissier into its own Pantry recipe (No. 126) and link to it. The dough was only ever readable as the first two lines of a custard tart’s ingredient table and step list, so it couldn’t be found by anyone wanting a plain short crust — and its numbers were excluded from the flan’s Analysis, which meant the site had no breakdown of it at all. It now carries its own: 55.4% structure / 24.4% fat / 18.0% liquid, which puts it beside Matcha Shortbread’s 51.5% / 23.5% with double the water — the difference between a dough that rolls into a 2-inch wall and one that crumbles. The flan’s ingredient table now opens with one line (“1 batch pâte brisée, lined and frozen”) and its step 1 links out; the two chills, the blind-bake-if-you-need-it procedure, scaling to a 9-inch, and why sucrée isn’t this dough plus sugar all live on the new page.
  • Add Crème Brûlée recipe — cream : yolk : sugar at 5 : 1 : 0.8 by weight (500g / 100g / 80g), set in a 325°F water bath to 170°F and torched to order. Sugar sits under the classic 20% of the cream because the crust adds 8g per serving; shallow ramekins are specified, since deep ones take 45–60 minutes and eat mostly custard. Notes cover yolks-only, the crust (blot first, plain granulated, torch not broiler), and that the scald exists to extract the vanilla bean rather than to dissolve anything — with paste you can whisk it cold. Analysis shows the same pure egg set as Purin with the fat inverted (30.6% vs 7.1%). Photo registry entry added.

2026-07-28

  • The article pages read as documents while the recipes read as designed, and the reason was measurable: an article’s section head was the smallest type on the page. It was set at $fs-small13px, smaller than the 15px body it introduced — hung in a 211px margin column that carried nothing else, so at 1440px the left third of every article was empty and four things sat on four different left edges (title 268, standfirst 268, head 268, prose 499). A recipe’s section head is 21px/700 flush left. Articles now borrow that grammar outright: 21px head over a hairline at the reading measure, prose directly beneath it, everything on the title’s own left edge. Four left edges → one, at every width from 320px up.
    • The measure did not change and was never the problem. 36rem, 71 characters per line, before and after — 576px of prose at every tier from 700px to 2560px. What changed is that the prose no longer starts 231px inside the title. This is the opposite of the usual fix: nothing got wider, the page just stopped having a private margin column for one 13px line.
    • The head’s rule is capped at 40rem, and that cap is the whole reason this needs no ultrawide branch. grid-column: 1 / 10 is a share of the field, and an article’s field is not a fixed width — it’s 904px below $split but 1263–1824px above it, where .post-content takes 19 of 26 columns beside the sticky rail. Uncapped, the hairline measured 942px at 1792 and 1363px at 2560 against the same 576px of prose: a rule two and a half times longer than the text under it stops reading as that section’s head and reads as a page divider. Pinned to 40rem it lands on the standfirst’s own right edge instead, so head, standfirst and prose share three left edges and two right ones at every width.
    • h2 + * { margin-top } had to be reset to zero, not left alone. That 56px lift existed to top-align the first block with a head hanging beside it in another column. With the head above its prose in the same column the same rule tears the first paragraph away from the head it belongs to — the offset was invisible support for a layout that no longer exists.
    • Article table columns are all left-aligned now. Every non-first column was right-aligned, which is correct for a figure and wrong for the last column, which in all 15 table-bearing articles is prose — “Most well-established range”, “Sweet doughs go lower”. Right-aligned prose is ragged on its left edge, so every row’s sentence started at a different x and the eye had to re-find the beginning on each line. tabular-nums stays, so 1.8–2.2% above 0.5–1.5% still lines up figure-for-figure.
    • Widening the gutter to match came for free and cost 41px of phone scroll. Moving padding-right: 1.5rem from the row label to every column but the last inflated each table by 13.6px per interior column: no page overflow — a phone table is its own scroll box by design — but more scrolling inside it, packing-caneles 99px → 140px and salt-in-baking 0 → 11px. Left-alignment was the change asked for; the gutter was collateral. Reverted to the original padding and the last column drops its trailing .8em instead, which is why every phone table now scrolls less than before: 169→159, 104→93, 99→89, 76→66, 53→43, 50→40, 48→38, 40→30, 33→22, 29→18px.
    • The dark screenshot caught a bug the geometry is structurally blind to. I authored the head’s rule as 1px solid $hair — the Sass token, #e4e4e4. That rule is (0,2,1), so the flat override in the dark block at the bottom of the file would have lost on specificity and every section rule would have stayed #e4e4e4 on #0c0c0d paper: the brightest line on a dark article, eight times per page. Exactly the trap .art-toc and .doc-nav already document, walked into anyway. var(--hair) flips with the scheme and has no specificity race to lose.
    • Verified against the Ruby Sass 3.7.4 compile substituted over the live stylesheet, on all 20 articles × 9 widths (320/390/545/700/900/1024/1440/1792/1920/2560): page overflow 0, table clipping 0 at every width from 545 up, and one left edge on every page. Phone table scroll is lower than baseline on all 11 affected articles; the single remaining page overflow (food-photography-at-home, 28px at 320px) is byte-identical to baseline and pre-existing. .wide-table keeps its nowrap prose column and sticky first column and scrolls 11px less. Control: recipes, the home directory, /changelog, /steering and /type at 390/700/1440/2560 are byte-identical old-vs-new — every box, every width, zero diffs. All four bin/check-*.py clean.
    • Chosen by rendering seven article layouts side by side at the real tokens rather than arguing about it — which is also what surfaced the flush-left option in the first place, and what made the 13px-vs-21px gap impossible to keep ignoring once both were on the same screen.
  • Article tables pushed the whole page sideways between 545 and 896px — the last horizontal scroll left on the site. canele-ratio-analysis 314px at 545px and still 159px at 700px; canele-bags 181px; salt-in-baking 90px; condensed-milk 49px. Zero at every width now.
    • Cause: two tier boundaries that had to agree were one pixel apart. white-space: nowrap on article table cells starts at min-width: 34.01rem; the scroll-container treatment that absorbed the resulting width stopped at max-width: 34rem. Below 545px the cells wrapped so nothing overflowed; above it they couldn’t and nothing caught it. The band only self-corrected at 897px, where the 12-column grid finally hands the table enough room. Fixed by unsetting nowrap in the 545–896px band — nowrap is right once there’s width for it, and a wrapped 1.8–2.2% is perfectly legible.
    • I nearly shipped the wrong fix, and the measurement preferred it. Extending the phone scroll box up to 56rem reports zero page overflow at every width — exactly what the check asked for. The screenshot killed it: the table becomes its own scroll box at column width and clips the prose column mid-word, 54–342px of it, so the Notes text is unreadable until you find a second nested scroll gesture. It converted a visible bug into an invisible one and the geometry called that a pass. The check now asserts scrollWidth > clientWidth on the table as well as on the page, so “no page scroll” can’t be bought with clipped content again.
    • The table also got shorter. Giving the prose column its width back took the widest rows from 5 lines to 3, so the whole table is more compact than the version that was overflowing.
    • Relocating the rule silently deleted the phone backstop — the compile check caught it: the scroll container is genuinely needed at ≤34rem, where even fully wrapped cells can beat the screen, and for a few minutes a comment claimed it was retained when the compiled CSS had zero copies of it. Restored, and it’s now explicitly documented as phone-only.
    • Verified against the Ruby Sass 3.7.4 compile at 390/544/545/560/700/896/897/1440/1900 on all four affected articles plus a recipe and two colophon pages: page overflow 314/181/90/49px → 0, table clipping 0, everything ≥897px unchanged, .wide-table’s sticky first column still sticky and headers still column-aligned. All four bin/check-*.py clean.
  • The colophon pages scrolled sideways on a small phone, and this page was the worst of them. /changelog overflowed 77px at 320px, 37px at 360, 7px at 390; /steering 13px at 320. Now zero at every width from 320 up.
    • Cause: a bare 1fr track has a minimum of min-content, so the longest unbreakable string on the page sets the floor for the whole layout. .doc-layout’s single column held 381px inside a 288px container on this page — because of a line in one of these very entries, 390/560/900/1440/1791/1792/1900/2560, which is one 338px “word” as far as the line breaker is concerned (a slash-run offers no break opportunity). On /steering the floor was a 294px Unsplash query string in a <code>. Fixed with minmax(0,1fr) in all three of this layout’s templates, which removes the floor entirely, so no future entry can reopen it.
    • The cap is only half of it, and shipping half would have looked fixed. Capping the track stops the grid growing, but a 294px token still spills out of a 288px column — the scroll moves rather than goes. So inline code gets overflow-wrap: anywhere and the prose gets break-word. anywhere specifically on the code: it’s the only value that also reduces the min-content contribution, so it holds even if a future template forgets the minmax. <pre> is excluded and still scrolls, which is right where a wrapped line would change the meaning.
    • overflow-wrap: break-word alone fixes nothing here — measured, as the obvious first guess. It breaks the visible line and leaves the track floor exactly where it was: overflow 13px before, 13px after. Worth recording because on a plain <p> it would have worked, so it reads like the correct fix and isn’t.
    • Same class of bug as the recipe-track fix above, from the opposite end: there the grid grew from a line that didn’t exist, here from content that couldn’t shrink. Neither showed as an element overflowing its own box — only as scrollWidth on the deployed page, which is why the phone sweep is now part of the check rather than a spot look.
    • Verified by injecting the Ruby Sass 3.7.4-compiled stylesheet over the live one and sweeping 320/360/390/560/736/737/900/1440/1791/1792/1900/2560 across all five doc pages plus a recipe and an article: doc-page overflow 77px and 13px → 0, and every tier at 737px and above pixel-identical — nav, body and prose widths unchanged on both sides of the split (224/437, 224/600, 224/660, 318/1263, 465/1824). All four bin/check-*.py clean.
    • The screenshot is the proof the numbers hid: at 320px the CONTENTS disclosure caret was pushed off the right edge of the screen and is now visible where it belongs.
  • Every block on a recipe was narrower than its own content column below 897px, and had been for as long as those tiers existed. Section heads, the step list, the notes and analysis bands, body paragraphs: 80px short at ≤544px (278px inside 358px on a 390px phone) and 20px short at 545–896px, on all 124 recipes. Now flush at every width.
    • The cause is one implicit track, and -1 not meaning what it looks like it means. The ingredient table asks for grid-column: 1 / 9. At --cols: 4 lines 5–9 don’t exist, so the grid generates four implicit columns to reach line 9 — 0px wide, but column-gap: 1.25rem still applies between them, so the grid grows four 20px gaps. Meanwhile 1 / -1 means “to the end of the explicit grid” (line 5, per spec — not a browser quirk), so every 1 / -1 block stopped four gaps short while the table, spanning to the real line 9, alone reached the edge. Fixed at the source: at 4 columns the ingredient column is the content column, so the rules asking for 1 / 9 there now ask for 1 / -1, the implicit tracks disappear, and every block agrees again.
    • The 8-column tier had the identical bug with a different culprit — found only because the fix for the phone tier made me measure 545–896px too. There 1 / 9 fits exactly, but .analysis asks for 1 / 10, growing one implicit track and one 20px gap. gamjatang, which has no Analysis band, measured flush at that width while every recipe with one measured 20px short — the tell that isolated it.
    • Two of the rules meant to prevent this had never run. .post-content table { grid-column: 1 / -1 } at the ≤56rem tier is (0,1,1) and loses to the .post-content > table at (0,1,2) that sets 1 / 9; the table only ever reached the edge because its own 1 / 9 happened to be the last line at 8 columns. Same for the phone tier’s copy. Both rewritten to match specificity — the fifth time the specificity ladder has silently decided a fix in this file, which is why each one now carries the reason inline.
    • Verified on the deployed pages at 320/390/544/560/896/897/1440: narrow blocks per recipe 6/8 → 0/8, worst shortfall 80px → 0 and 20px → 0, implicit 0px tracks 4 → 0 and 1 → 0, zero new overflow, articles and colophon byte-identical at every width, and the tiers above 897px unchanged. Compiled against Ruby Sass 3.7.4 (what Pages uses). All four bin/check-*.py clean.
    • Also looked at it, which the numbers alone wouldn’t have shown: the steps now run the full measure with their numbers at the true right edge, and the same screenful holds 7 steps instead of 6.
  • A recipe lede now takes the ingredient column instead of the whole content width. At 21px across the full 904px box it ran 83 characters per line — the widest measure anywhere on the site, in the largest body size, on the one paragraph a reader meets first. Swept all 25 ledes: 24 of them were over 75cpl, median 87, max 90, while a step in the same page reads 72 and the article standfirst is capped at 40rem. grid-column: 1 / 9 is the same track range the ingredient table directly below it already uses, so the lede’s right edge now lines up with the table’s instead of overhanging it by ~300px. 596px ≈ 55cpl at 1440; 556px at 900.
    • Scoped below $split, and that scoping is the whole fix. The ultrawide spread has two branches — recipes with 6+ steps put their left bay at 1 / 9 (which this agrees with), but the shorter ones take the half-split at 1 / 13. An unscoped rule outranks .post-content > * on specificity, so whipped-cream’s lede ended up 286px short of its own ingredient table’s right edge at 1900px — the exact misalignment the change exists to remove, reintroduced one tier up. Caught by asserting the edge match at every tier rather than only at 1440.
    • Nothing else moved. Articles keep their own 1 / 10 + 40rem standfirst (:not(.section-articles)), and the colophon pages are untouched. Verified on the deployed pages at 390/560/900/1440/1791/1792/1900/2560: recipes narrow and align below the split, are byte-identical above it, articles and colophon identical at every width, zero page overflow introduced, zero JS errors.
    • Compiled against Ruby Sass 3.7.4 — the version GitHub Pages actually uses, not the local one — to confirm $split - .0625rem resolves (max-width: 111.9375rem) and the :not() survives. bin/check-selectors.py and bin/check-links.py clean.
    • Chose the measure by rendering all five candidates side by side at the real tokens rather than arguing about rem values, which is also what surfaced a separate pre-existing phone bug, left unfixed here: the ingredient table’s 1 / 9 adds four 0px implicit tracks to the 4-col grid, and 1 / -1 resolves to the end of the explicit grid — so every 1 / -1 block on a recipe stops 80px short of the content edge at 390px. That’s every step, head and note on every recipe, so it wants its own change; the lede is 278→358px here as a side effect of no longer being one of them.
  • All five colophon pages now read like the rest of the site at ultrawide: a sticky contents rail in the outer margin and section heads hanging beside their own prose. Only /steering/ had the rail before, and it spent it badly — a 224px nav plus a 640px column of prose stranded in an 1825px wrapper, 969px of it empty. changelog, drafts, photos and type had no rail at all and 1021px of empty right side. One treatment across all five, the same grammar the articles and the home directory already use: the rail takes columns 1–6, the body takes 8–27 and is itself a 12-track grid, and a .doc-body > h2 hangs in tracks 1–4 beside prose that starts at 4. Dead paper drops to 379px on steering and 28px on the other four. The reading measure does not move — 612–632px before and after, at every width.
    • display: contents is what makes the heads hang. The rail and the body are wrapped in a .doc-layout div, so they were grid items of that div, not of the page grid, and a head placed in “column 1” would have been column 1 of a 2-track box. Promoting the wrapper out of the box puts both children on the 26-track .wrapper grid, on exactly the lines the article rail (1/6) and article prose (8/27) already sit on.
    • The rail is clamped to the viewport, which the article rail doesn’t need. An article has 5–8 heads; the changelog has 28, which is 698px inside an 866px window and grows by one every time something ships. A sticky element does not scroll, so six more entries would have put its last links permanently below the fold with no way to reach them. max-height: calc(100vh - 3rem); overflow-y: autoauto, so the four short pages never draw a scrollbar.
    • Two pages had to earn their heads. type.html had zero <h2>s — its five section labels were <p class="spec-label">, styled to look like heads — so it would have rendered a rail with nothing in it; they’re real heads now and the local styling is gone. photos.html was setting its own 26px ruled head borrowed from the home directory, and pinning all three of its blocks to grid-column: 1 / -1, which ties .doc-body > * on specificity and wins on source order — it would have dragged every head back out of the margin. Both pages now take the shared head. Freeing the ribbon also grows the photo grid from 832px to 991px.
    • A head’s rail label is its own text, not everything inside it. photos.html appends a count to each head, and h.textContent glued them: six entries that each read “mains3/22 with photo”. The builder now walks the head’s direct text nodes and honours [data-toc-skip] on a child, so a nested <em> or <code> in a head still counts but the count stays out of the jump list. Found by reading a screenshot; every geometric assertion passed on the version with the bug.
    • On phones the rail is a collapsed disclosure, and it had to be, because it was costing a screen. Steering’s list is 732px and the changelog’s 654px against an 800px viewport. Collapsed it costs 46px on all five — one 44px tap target and a caret that flips, identical to the article’s treatment at its own tier. Ships open and closed by script, for the reason recorded yesterday: a closed <details> hides its content via UA content-visibility, which CSS can’t portably override. A page with fewer than two heads gets no rail and its body takes the freed column.
    • The highlight is a midline test, not an IntersectionObserver. Same fix as the article rail, and it matters more here: the marginalia layout makes these pages short (drafts is 1.5 screens), and a -75% top band is unreachable on a page under two viewports. Verified that the last section highlights on both long pages at 866/700/560px window heights.
  • Five bugs that were already live on the site, every one found by measuring the deployed page rather than reading the source:
    • The recipe spread was leaking onto the colophon pages. The ultrawide #steps ~ * / #analysis ~ * placement rules were scoped body:not(.section-articles) only, so a colophon page with a “Steps” head — which steering.md has, because it documents how to write steps — took recipe geometry. All three sites now carry :not(.section-colophon) too, and the comment says why it’s load-bearing rather than belt-and-braces.
    • /steering/ scrolled horizontally on every phone. Its @media (max-width: 46rem) block set grid-template-columns: 1fr on the layout and stopped, but a grid item placed in a column the template doesn’t define gets an implicit track — so the nav and the body kept their wide placement and built 698px of layout inside a 390px viewport. It read as “collapsed” in the CSS and never was. grid-column: 1 on both is what actually collapses it.
    • Two paragraphs of this changelog were rendering as tables. kramdown parses a GFM table with no header row and no delimiter line — one | anywhere in a paragraph is enough to split it into cells — so “12 | 12” and “a name | list pair” each became a <table>, and because a cell is its own parse context the second one printed its ** asterisks literally. Confirmed on the live page, not inferred. Escaped as &#124;.
    • bin/check-pipes.py fails on any bare | in Jekyll-rendered prose, ignoring fenced code, inline code, Liquid, real tables (a row that starts with |), and anything under a dotted path that Jekyll never renders. Red on the two bad lines, green after.
    • Numbered lists on the doc pages were being rendered as recipe steps, and it was shredding them. .doc-body is .post-content, so .post-content ol > li { display: grid } — the two-track step row — applied to every list on these five pages. A grid container blockifies its inline children, so each <code> became its own grid item, auto-placed into the 2rem step-number track and then into implicit tracks: steering’s “Resolution order” list collapsed into a one-word column running 244px past the right edge of a phone, with a recipe step counter (01, 02) printing beside documentation prose. The doc lists now reset all four properties the articles already reset — display, list-style, the ::after counter, and the inline li > p.
      • Only scrollWidth could find this. No element’s own box overflowed the viewport — every candidate’s right edge measured ≤390px — because the overflow was in the grid’s implicit tracks. The per-element sweep returned zero hits on a page that scrolls 244px sideways.
      • The first version of the fix compiled, would have shipped, and changed nothing. .doc-body li is (0,1,1) and loses to .post-content ol > li at (0,1,2), so the measurement came back 11 grid li → 11 grid li. Written as .doc-body ol > li it ties on specificity and wins on source order. Fourth time the specificity ladder has decided a fix in this file, so the rule now carries the reason in a comment.
      • Verified red→green on all five pages × 3 widths (overflow 244→0, grid li 11→0, all 84 list items back to list-item, measure and all 57 margin-head pairs unchanged), and every .post-content child’s geometry on four recipes plus an article is byte-identical old-vs-new at 1881/1440/390 — the step list still belongs to the recipes.
  • Verified across all five pages × 5 widths (1881/1440/1024/768/390) on the compiled stylesheet: zero horizontal overflow, zero collapsed columns, and all 57 margin-head pairs top-aligned with their prose to the pixel at 1881. Every tier asserted rather than assumed — phone is closed with a caret and a 44px tap, mid is open with no kicker and no caret, ultrawide is open with a ruled kicker — and dark mode audited rule-by-rule at 1881 and 390, because the new .doc-nav rules live inside a media query where a flat $ink hairline would have stayed #111 on #0c0c0d paper at ~1.04:1. They’re authored against var(--ink)/--grey/--hair for that reason. bin/check-selectors.py clean.
  • Two bugs that had been live for as long as the articles have existed, both found by sweeping the deployed site rather than the one page I’d changed. 158 URLs × 4 widths × light and dark, measuring documentElement.scrollWidth and the composited contrast of every text node. The sweep also produced three false positives, each dismissed with evidence: the recipe Notes band’s <h2> is a deliberate sr-only head, inline <code> is rgba(17,17,17,.06) and reads as 1:1 unless you composite the alpha stack down to the paper it actually paints on (~#f0f0f0), and the “prose overruns its measure” heuristic was catching short ingredient group labels at grid-column: 1 / -1.
    • Five article pages scrolled sideways — up to 1452px — because max-width does not bound a <table>. A table’s used width can never fall below its min-content width, and white-space: nowrap (set for numeric alignment) makes each cell’s min-content its full unbroken string — so one prose cell in a Notes column set an 886px floor and dragged the table to 1446px inside a 576px grid slot. The max-width: 36rem on that rule had never done anything. Worst was /a/canele-bags at 1024px; /a/packing-caneles scrolled 699px at 1440. Total page overflow across the 15 table-bearing articles × 4 widths: 7275px → 0, light and dark.
      • The diagnosis was proven in the browser by toggling nowrap off on the live page and watching overflow go 214 → 0, not inferred from reading the rule. The fix has two halves: prose columns wrap (white-space: normal on :last-child, which alone cleared 5 of the 6 pages), and a table with 4+ columns takes the full grid instead of the 36rem reading measure — :has(> thead > tr > th:nth-child(4)), so it’s decided by counting header cells in CSS. A stale comment in the stylesheet had claimed this already worked via a --w custom property; --w never existed, so 14 of the 15 table-bearing articles had been getting no wide treatment at all.
      • display: block + overflow-x: auto on the table is the obvious fix and it is wrong. It cleared all 7275px, which is exactly why it nearly shipped. A block-level table generates an anonymous inner table box that shrink-wraps its content, so width: 100% sizes the scroll box but not the rows: measured 336px of hairline running past the last cell on /a/dialing-in-remi and 338px on /a/condensed-milk. That trades page scroll for ragged rules, which is not a trade. Caught only because the harness measured row width against box width instead of stopping at “overflow is zero.”
      • /a/canele-bags still overflowed 659px after that, because its 7- and 8-column tables are too wide for even 26 columns of grid. Those three are wrapped in the existing .wide-table div — a real element, so it can be the scroll box while the table inside stays display: table and keeps its sticky first column. The wrapper is now for 6+ columns; 4–5 are handled without markup.
      • Verified in shipped form, not by appending a <style> to the page: the compiled stylesheet is substituted for the live one by request interception, so the two later grid-column overrides at the 56rem and 34rem tiers sit where they really will. (They lose on specificity — three classes plus :has() to two — but that had to be measured, since source order has decided four previous fixes in this file.) The remaining raggedness at 390px is byte-identical old-vs-new: it comes from the pre-existing phone rule. Right-aligned wrapped prose already ships on phones today, so desktop now matches it rather than inventing a treatment. Control: 40 renders across recipes, home, and all the colophon pages are byte-identical — the change is scoped to body.section-articles.
    • 50 of the 79 riso ink swatches printed white text on a light swatch; the worst was legible as a shape and not as text. Fluorescent Yellow rendered its own hex #ffe900 in white at 1.24:1. 27 pills were under 3:1 and 50 under 4.5:1. Every swatch now carries .pill-dark where ink-on-swatch beats white-on-swatch by WCAG contrast, taking the worst case to 4.53:1 — all 79 at AA for normal text.
      • It has to be authored into the markup, per swatch. CSS has no relative-luminance test and color-contrast() ships in no engine, so the comparison is done once and stamped. A flat ink-on-everything would have broken the 29 dark swatches instead; the choice is per-colour by construction. The text colour is $ink and deliberately not var(--ink) — the swatch background is a fixed real ink in both schemes, so the text over it must be fixed too, or the pair flips in dark mode and half of them go unreadable again.
      • The White swatch was a hand-authored special case carrying its own color: #888 (3.54:1) and was the only pill left failing after the sweep; it takes the shared treatment now. Found by counting 79 pills against the 78 the generator had rewritten.
  • The stylesheet link now carries the deploying commit, so a CSS fix can’t be hidden behind a cached file. Pages serves /assets/main.css with max-age=600 at a URL that never changes, so for ten minutes after a build a returning visitor gets the old stylesheet against the new markup — which is worse than either alone. Both of the last two bugs were CSS-only, meaning the stale cache showed the bug on a site that was already fixed, and “I still see it” and “it isn’t fixed” are indistinguishable from the outside. ?v={{ site.github.build_revision }} (from jekyll-github-metadata, bundled in the github-pages gem) changes on every build, so there’s no manual version to forget to bump; on a build without the plugin it degrades to a bare ?v= — same file, no harm. This is also what would have made yesterday’s live-CSS diff unnecessary as a debugging step.
  • GitHub Pages’ Sass had been silently deleting a selector, and 70 recipes shipped broken for a day because of it. The stylesheet is built on Pages by jekyll-sass-converter 1.5.2 → Ruby Sass 3.7.4, which predates :has(). Standalone it passes an unknown selector straight through, so .a:not(:has(> .x)) at the top level is fine — but the moment it has to rebuild a selector, which is what nesting a rule inside a parent block (or using &) requires, it re-parses, chokes on :has inside :not, and emits the rule with the whole :not(…) dropped. No warning. Exit 0. Green build.
    • What that did: yesterday’s no-Analysis notes rule went live as an unconditional .post-content > #steps ~ .notes, which ties on specificity with > #analysis ~ .notes and wins on source order — so all 70 recipes that have an Analysis took the full-width 4-up band meant for the 47 that don’t, and the notes text printed directly on top of the “analysis” head. Every geometric assertion in yesterday’s sweep passed, because they all ran against a locally-compiled stylesheet. Only diffing the live CSS selector-by-selector against a local compile found it.
    • The fix is a form the compiler won’t touch: both :not(:has(…)) rules are now written out in full at the top level of their media block, and the step-split trigger is an interpolated string variable (interpolation is textual, never re-parsed). Confirmed by installing Ruby Sass 3.7.4 locally and compiling — the output is now byte-identical to the previous live CSS apart from the intended changes, and both :not(:has(…)) survive.
    • bin/check-selectors.py flags any :not(:has(…)) nested under a parent selector, and prints the exact Ruby Sass command to verify against the compiler Pages actually uses. It fails on yesterday’s commit and passes on this one, so it isn’t vacuous. Run it before pushing a stylesheet change.
    • Lesson worth the entry: “it compiled” and “it compiled the same way the host will” are different claims. Two different Sass implementations agreed with each other and both disagreed with production.
  • The ultrawide split is now decided by the recipe, not by the breakpoint. An equal 12 | 12 spread assumed both halves are about the same height, and they aren’t: ingredients are almost always shorter than steps, so the right column ran past the left by 256px on average and 783px at worst (the chiffon cakes and gâteaux — 12 steps beside 10 ingredient lines) across all 124 recipes at 1881px. No single split fixes that, because the geometry that helps a 12-step cake hurts a 3-step cocktail. So the page picks: a recipe with six or more steps puts ingredients in an 8-track bay and runs the steps in two columns across the remaining 17; a shorter recipe renders exactly as before. Overhang drops to 114px mean / 336px worst, and the recipes over 300px go from 38 to 1.
    • CSS can count, so there’s no script. :has(> #steps ~ ol > li:nth-child(6)) is “six or more steps” — I checked the selector against the real DOM step count on 124/124 recipes, and the group-count selector against the real table count on 124/124 too.
    • The threshold is measured, not chosen. Splitting at 4+ leaves the least empty paper (103px mean) but gives 27 recipes a column of only two steps, which doesn’t read as a column — it reads as a widow. 6+ is the lowest threshold where that never happens: 0 thin columns at 1792/1881/2560/3440, 73 of 124 recipes split.
    • Three or more ingredient groups opt out. Groups stack, so each one adds height to the left half; past two groups the left half is the taller side and halving the steps opens a gap under the steps instead of closing one. Measured all three cutoffs: opting out at 2+ groups gives 126px mean / 420px worst, at 3+ gives 114 / 336, and never opting out gives 121 / 521 (tonyu-nabe, three groups beside six steps, overshoots by 521px). 3+ wins on both mean and worst case. The census is 106 recipes with one group, 12 with two, 6 with three or more.
    • columns, not a second grid. A step list has to balance across the two columns (7 steps → 4 + 3) and only multicol does that without measuring in JS; multicol is also column-major, so visual order stays source order (asserted on every recipe at every width). The reading measure does not move: 26 tracks read as three 8-track bays (8 + 1 + 8 + 1 + 8), and one bay is 548px at 1881px — which is the 34rem every step was already capped at. The second column is free.
    • The step number moves to the head of its step. A right-hung number works in one column because nothing is to its right; in two, the next column’s text sits 20px away and 01 reads as if it belongs to step 05. This was found by looking at a render, not by measuring — every geometric assertion passed on the version with the bug. Leading the step removes the ambiguity without spending measure on an alley (the alternative, one track of gutter between the columns, cost 34px of measure). Same convention the notes band already uses.
    • Ingredients are not also split. A single <table> won’t fragment across a CSS column (measured: a 20-row table in a column-count:2 box stayed one column), so the ingredient side could only divide at a group boundary — and doing that measured as a wash against today (167px vs 163px on the multi-group recipes) while cutting the step measure to 510px. Dropped rather than shipped for symmetry.
    • The > *:has(~ #steps) form is load-bearing for the same reason it is elsewhere in the file: a blanket #ingredients ~ * chain carries three IDs and outranks > #steps ~ ol at two, which dragged the step list into the left bay — 94 overlaps and the measure collapsed to 226px. The one-ID-beats-any-number-of-classes trap, third time.
    • Verified on the compiled stylesheet, not the source: all 124 recipes × 4 widths ≥ $split on five invariants (two columns, number leading its own step, source order = visual order, no overlap between any pair of blocks, no horizontal overflow) — 0 failures — plus a byte-comparison of every .post-content child’s geometry old-vs-new at 390/768/1024/1440/1791, which is identical on all 124 at every width, so nothing below the breakpoint moved. Dark mode checked on a split recipe.

2026-07-27

  • Ultrawide follow-ups — two layout bugs in the spread that shipped an hour earlier, both caught on real pages:
    • Multiple ingredient groups rendered stacked on top of each other. A single #ingredients ~ table { grid-row: 3 } matched every following table, so tonkatsu’s three groups (main / quick sauce / to serve) painted in the same row, and grid-auto-flow: dense backfilled their ### subheads into the empty row 1 above the “ingredients” head. This was the common case, not an edge one: 24 recipes carry more than one table before Steps and 13 carry subheads. The left half now gets one row per block in source order, assigned by counting preceding siblings (#ingredients ~ * ~ *…) — which walks source order without caring what the elements are, the same reason the halves are assigned by #id ~ * rather than nth-child. Emitted shallow-to-deep so the deepest match wins. The :has(~ #steps) qualifier is load-bearing: without it the chain also matches #steps, the steps list, .analysis and .notes (all siblings of #ingredients too) and at (1,1,0) outweighs .post-content > .analysis at (0,2,0), dragging the right half and the superrow out of place — the same one-ID-beats-any-number-of-classes trap as the stranded notes. The sweep then turned up the mirror bug on the one recipe with two step lists (kombu-dashi: cold brew / simmer): both took grid-row: 3 / span 60 and overlapped, so a recipe with a second list now opts out of the deep span and auto-places in order.
    • The superrow separator sat on top of the last step. Its 80px of air was below the rule rather than above it (the .notes base rule’s top margin landed under the separator), so the hairline read as an underline on the steps instead of the top edge of a new band — 0px above, 81px below. The margin moves onto the rule’s own row and the notes bind to it with the same 24px the Analysis head uses: now 80px above, 25px below, on every recipe.
    • Verified across all 124 recipes × 6 widths on two independent invariant sets — added no-overlap (every pair of blocks, not just tables) and source-order-equals-visual-order checks for the left half, plus air-above/air-below bounds on the separator — 124 pass · 0 fail. All twelve sample recipes fail those same checks against the shipped stylesheet, so the assertions aren’t vacuous. One assertion from the earlier pass (GAP-ING) was itself wrong — it measured the head to the first <table>, which flags the correct layout on the 13 subhead recipes — and now measures the head to the next block; the bug it guarded is still covered by two other checks.
  • Ultrawide layout (≥ 112rem / 1792px) — one recipe on one screen, and a directory that uses the whole monitor. The threshold is a single Sass variable ($split, top of main.scss) that three media queries read, so the breakpoint can be retuned in one place. It has to be Sass, not a custom property: @media can’t evaluate var().
    • Grid: --cols goes 12 → 26 at $split — two 12-column halves with a 2-track alley — so each half is the width the whole page used to be (12 tracks ≈ 71.6rem at 2560px ≈ the old 72rem ribbon). Below $split nothing changes; the existing 12/8/4 tiers are untouched.
    • Home directory: each category is now a name | list pair — the name hangs in a left margin and both hang from one shared top rule (a band edge, not an underline belonging to the name) — and this is the layout at every width above the phone tier, not just ultrawide. The page is full-bleed (max-width: none); the width freed by dropping the ribbon is exactly what buys the list more flow columns, so long categories (sweets = 51) grow in width, not height. At $split the directory pairs up into two page-columns reading “name list / name list”. That’s CSS columns: 2 on .dir, not a grid: the browser balances the two columns so they end level, whereas a grid row is as tall as its taller half and the categories are wildly unequal (sweets 51 vs colophon 5) — pairing those in a grid leaves a hole the height of the difference. break-inside: avoid keeps a name from ever splitting off its list. The pair survives down to the phone tier, where the name column drops and it stacks.
    • Recipe spread (“mise en place”): ingredients LEFT, steps RIGHT, then analysis and notes side by side in their own band below both — a “superrow” separated by one hairline spanning both halves and the alley (the notes block’s own shorter hairline is switched off; two rules 24px apart, one of them shorter, read as a mistake). Placement is driven by h2 ids (#steps ~ *, #analysis ~ *), never nth-child: .post-content is a flat kramdown sequence whose shape varies — 118 recipes open with a table, 10 with an ### subhead, 1 with a paragraph, 6 carry a Grind block, 53 have no Analysis — and any positional selector breaks on those. Steps keep their 34rem measure; the extra width buys a second column of content, never a longer line. The 47 recipes with notes but no Analysis take the full band 4-up instead of stranding an empty half. The hero photo becomes a fixed 30rem cinematic band (uncapped, a 16:9 figure is 1440px tall at 2560px — you’d scroll past a photo to reach the recipe the spread exists to fit on one screen).
    • Article rail: articles get a sticky contents list in the outer-left margin, built from their own <h2>s, with the section in view highlighted. The prose does not get wider — it keeps its 36rem measure and marginalia subheads; a two-column article body was rejected outright (prose that snakes up and back down a 2560px page is worse to read, not better). The same single <ol> renders as a collapsed disclosure above the standfirst on phones (≤34rem, 44px taps) and is hidden in between, where the marginalia subheads already show the structure. It ships open and is closed by script on the phone tier, not the reverse: a closed <details> hides its own content via UA content-visibility — which CSS can’t override — so a default-closed element would render the ultrawide rail as a bare “Contents” label, and it also degrades correctly with JS off. Articles with fewer than 2 sections drop it entirely. Recipes get no rail.
    • Three bugs found and fixed by the verification sweep rather than by eye, all worth recording because each has a reusable lesson: (1) grid rows couple the two halves — column placement alone isn’t enough, since both halves auto-place into the same implicit rows, so a standfirst offset one head and a 9-step list stretched whipped-cream’s short ingredient table’s row and left “ingredients” floating 380px above it; fixed with explicit rows, a deep span for the steps list, and align-self: start (an explicit row is also needed on each half’s first block, because the row dense the superrow requires backfills empty row 1 and jumped the table above its own head). (2) One ID outweighs any number of classes — the no-Analysis notes fallback at (0,3,0) silently lost to > #steps ~ * at (1,1,0), so 47 recipes kept their notes pinned in the right half; adding #steps ~ to the fallback buys the ID weight. (3) A flat dark-mode override can’t reach a nested rule.art-toc { … } (0,1,0) loses to the same rule nested under body.section-articles (0,2,1), which left #111 hairlines on #0c0c0d paper at ~1.04:1, i.e. invisible; both new components are now authored against var(--ink)/--grey/--hair, which already flip with the scheme, so there’s no override to lose. The article highlight also had to stop using the doc-nav’s IntersectionObserver (its -75% top band is unreachable on a page shorter than ~2 viewports — and the rail is what makes articles that short, so the highlight stuck on section 2 of 5 for the whole second half of a 2560×1400 screen); it’s a midline test with a bottom clamp instead.
    • Verified in headless Chrome across all 124 recipes × 6 widths (2560/1792/1791/1512/800/390) on nine geometric invariants — heads aligned, no block above its own head, no stranded half, one separator not two, no horizontal overflow, measure capped — plus real WCAG contrast ratios for every new rule and label in both schemes, and the phone disclosure’s collapsed height and tap targets. Each new assertion was first proven to fail on the pre-fix stylesheet (the stranded-notes check: 47 fail / 77 pass), so the green isn’t vacuous; two failures that reproduced identically on the live deployed CSS were confirmed pre-existing and left alone.
  • Home index (touch tiers): the hairline under each recipe name no longer reads as underlining the row below it. Splitting the 44px tap target’s padding evenly put the rule ~20px beneath its own word but only ~10px above the next one — at a 2:1 ratio it detaches from the name it belongs to and stops reading as an underline. The padding is now asymmetric and deliberately sums past 44px (1.4rem 0 .3rem; if the padding box is shorter than min-height the content box stretches and dumps the slack back under the text, so symmetric padding can’t fix this): the rule sits 7.7px under its word, matching the desktop tier’s 6.1px. All 149 index links stay ≥44px, and the phone tier’s separate override is gone — it inherits the tablet rule. Cost: two-line entries grow 53→61px, so the phone page is ~2% taller.
  • Fix recipe steps collapsing to a single ~80px column on five recipes (tea jelly, thyme streusel, cold brew tea, coffee jelly, bay leaf panna cotta). The step li was a 12-track subgrid and only li > p carried the 1 / -2 reading span — but kramdown emits <p> only for a loose markdown list (blank line between items). A tight list (1. / 2. / 3. on consecutive lines) emits bare <li>text, which becomes an anonymous grid item no selector can reach, so it auto-placed into column 1 and wrapped at ~46–88px instead of 538px. The li now owns two tracks (minmax(0,34rem) auto, space-between) so the measure is the track itself and the layout no longer depends on how kramdown wrapped the text; the step number moves from the -2 / -1 span to track 2, and a ul nested inside a step spans track 1 rather than the parent grid’s line 9. Verified in headless Chrome at 1512/800/390px: the five affected pages go from 3–5 wrapped lines to 1, four control pages (gyoza, canelé + two articles) are geometrically byte-identical, and the phone tier — which was never affected, its ol having always had two tracks — is unchanged.

2026-07-24

  • Responsive / mobile pass (adversarial persona-panel review — mobile-first user, a11y/junior, design director, typographer, minimalist, Ive/Rams reductionist, perf engineer):
    • Ship-blockers: home masthead is now flex-wrap‘d ≤34rem with the byline dropping to its own line (was a no-wrap flex row that forced a horizontal scrollbar on every portrait phone — WCAG 1.4.10); article-table white-space:nowrap is gated to ≥34rem so prose tables wrap on phones instead of blowing past the viewport; the dark-mode focus outline is recolored to dark ink (was #111 on #0c0c0d ≈ 1.05:1, effectively invisible — WCAG 2.4.11); the hero photo is no longer loading="lazy" and carries fetchpriority="high" (it’s the above-the-fold LCP element, so lazy-loading it delayed LCP).
    • Mobile UX: the recipe step-number column is narrowed from a full 1fr (~25% of a 4-col row) to a 2rem track ≤34rem, reclaiming ~54px of reading measure while keeping the far-right number; hero-title leading loosened to 1.02 + text-wrap:balance ≤56rem so long lowercase titles stop colliding descenders/accents into the next line at the clamp floor; home-index tap targets (44px) now extend through the tablet tier (were phone-only).
    • Polish: .wrapper side padding drops to 1rem ≤34rem (was a fixed 1.75rem = 17.5% of a 320px screen); preconnect to images.unsplash.com to shave ~300–600ms off hero LCP on 4G; the wide comparison table’s row-label column is now position:sticky so metric labels stay visible when scrolling right; .grind/.analysis pinned full-width at 4-col (were spanning to grid lines 9–10 that don’t exist there); .grind setting column wraps on phone; .notes h2 moved from display:none to visually-hidden so screen readers still announce the section.
    • Cleanup: delete the dead .pill-sample::before rule (nothing set data-num); remove the empty _includes/footer.html and its per-page include; stop tracking the generated cafe/site/*.html build output (the build-review.js generator + today-sheet-setup.md source stay tracked).
    • Follow-ups (verified in DevTools mobile emulation at a true 390px): pack the home index 2-up ≤34rem — columns:12rem collapses to a single column at phone width, so the directory (sweets = 51) was an endless single-file scroll; two even columns roughly halve the page height. Make bare (non-.wide-table) article tables their own overflow-x:auto block ≤34rem — gating nowrap wasn’t enough because a 4–5-column table’s min-width still beat the screen (canele-bags overflowed the page to 563px, packing-caneles to 425px; both now 390px, no page scroll). Confirmed live: masthead no longer overflows, the longest title (“big-batch hong kong milk tea canelé”) wraps to 3 lines with leading 42.4px > cap 41.6px (no glyph collision), the wide-table row-label column stays pinned when scrolled, and the dark-mode focus ring is now 17.4:1 (was ~1.05:1).

2026-07-22

  • Homepage masthead: remove the heavy 3px rule under the wordmark — it was the only 3px divider on an otherwise-1px-hairline site. The giant wordmark + whitespace carry the separation now (Ive pass, chosen from a 6-option chooser).
  • Homepage: remove ornamental category index numbers (01–08) — the category name carries the meaning; order is visible from position.
  • Minimalist cleanup pass (delete unused / dead / outdated / leaked):
    • Security: remove docs/ (9 internal Amazon engineering docs — BYOD/Stencil/Stores-Designer, a CSS-race postmortem, env-ID files) and byod-cr-explainer/ — both were committed to this public repo AND publishing to halfcadence.github.io/recipes/docs/…. Gitignored + excluded so they can’t return. Also excluded bin/ (dev tooling) and .playwright-mcp/ from the build.
    • Junk: remove .playwright-mcp/ (83 browser-tool debug logs, 11MB) + gitignore it.
    • Retired archive (~50MB, the bulk of the repo): delete assets/illustrations/ (249 SVG + AI-riso PNG files), _data/illustrations.yml, and the /illustrations + /illustrations-riso + /colors gallery pages — all delisted (reachable from nowhere in the nav), preserving a retired risograph/EJ system. The risograph article stays as the record. Fixed the now-dead references in type.html, steering.md, AGENTS.md, README.md and the dead swatch selectors in the print CSS.
    • Vestigial config: drop instagram_username and header_pages: [] (zero consumers).
    • Outdated docs: README + colophon prose still described the retired Experimental-Jetset “one accent per section” system as current — corrected to monochrome Müller-Brockmann.
  • Second review pass (finish the remaining audit items):
    • Per-page descriptions: each page now gets a unique meta/share snippet instead of the shared tagline — explicit front-matter description: wins, else a synthesized line (recipes: “title — a {category} recipe … No. N”; articles: a note line), else the site default. Documented the description field in steering’s Front Matter; added a real one to the canelé article.
    • Tokens: expose the Sass palette + type ramp as CSS custom properties (--ink, --fs-cat, …) on :root with a dark override, and convert the type.html / photos.html inline styles to var(--…) — so their chrome stays on the ramp and dark mode flips automatically (deleted both pages’ per-page dark blocks). Swatch/ramp-demo values stay literal (they’re content).
    • A11y: byline links (halfcadence / gh) now carry a persistent underline, not color/weight alone (WCAG 1.4.1). Add PNG + apple-touch favicon fallbacks alongside the SVG.
    • Steering doc nav: hide the sticky section nav when a toc page has no <h2>s (was an empty labeled landmark); prefix auto-ids toc-h-; fix the nav floating ~24px above the body — its first row now aligns to the lede.
  • Adversarial review fixes (design-director / nitpicky / junior / CEO personas + technical audit):
    • Sharing: replace the broken og:image (a relative-URL SVG that no platform renders) with a real 1200×630 PNG served at an absolute URL; a recipe now shares its own hero photo, others share the site card. Add twitter:card: summary_large_image + twitter:image. Switch jekyll-feedjekyll-seo-tag (recipes are pages, not posts, so the RSS feed was empty/dead) and rewrite the site description, which still read “two inks, eight colors” (the retired riso identity) on every page.
    • Print: scope the dark theme to screen so a recipe printed from a dark-mode OS is black-on-white, not near-white-on-white; hide the hero photo when printing; narrow print-color-adjust: exact to the archived riso swatch pages only.
    • Copy/detail: fix the breadcrumb rendering “Soups & hot pots” (a capitalize filter was lower-casing the tail) → “Soups & Hot Pots”; remove a stray “.” (an unstyled ---) on the canelé article and add a proper hr hairline rule; recolor the 404 spot-blue to monochrome grey.
    • A11y: make the home wordmark an <h1> (was none); add aria-labels to the “gh”/personal-site byline links; fix light-mode caption contrast on /photos (#9a9a98#6b6b6b, ~2.8→5.3:1).
    • Mobile/perf: single-column ingredients on phones (was cramped 2-col at 390px); ~44px tap targets on the home index; responsive hero srcset + width/height so phones don’t fetch the 1600px image.
    • Housekeeping: remove dead CSS (.post-title--plain, .num/%num, the $serif alias, the undefined .cat--wide class) and the orphaned og-image.svg.
  • Add bin/check-numbers.py — validates the recipe number: sequence (every r/ page numbered, contiguous 1..N, no gaps/duplicates) and prints the next number to assign. Wired into AGENTS.md; current sequence is clean (1..124).
  • Homepage recipe count: compute the masthead “124” at build time from the number of pages in r/ instead of hardcoding it, so it can’t drift out of sync when recipes are added. Renders identically (124) today. Documented in steering.
  • Homepage byline: give the flat, unlinked “by / halfcadence” more weight and make it useful — now by halfcadence / gh on one baseline, with halfcadence linking to andrewshiau.com (ink) and gh to github.com/halfcadence (grey). The two external links open in named tabs (target="halfcadence-site" / halfcadence-gh", rel="noopener") so repeat clicks reuse one tab. Masthead wordmark + count unchanged.
  • Add bin/check-links.py — validates every internal link (index ./ anchors + relative_url body links) against real files and declared permalinks; documented in AGENTS.md as a pre-push check. Full audit run: all internal links resolve.
  • Remove type-comparison.html — a dead, unlinked colophon page that still described the retired Bricolage Grotesque type system (site is Helvetica-only now). Nothing linked to it.
  • Header: fix the nav/category “stutter” — the top-bar “← index” kicker and the category eyebrow (“MAINS”) directly below it were the same grey uppercase 11px label stacked, reading as a duplicate. Fold the category into the nav as a breadcrumb (← index / mains, index grey + category ink) and drop the separate .section-label eyebrow. Applies to all inner pages (recipes, articles, colophon).
  • Fix 38 broken internal links in Drafts — the drafts index and all 6 draft pages used bare ](/drafts/…) / ](/r/…) / ](/a/…) paths, which drop the /recipes baseurl and 404 on prod (e.g. the P-Bass Mute and Ttukbaegi links, the “recipe index” link). Wrap them all in relative_url. (r/ and a/ were already clean; drafts/ had been missed.)
  • Type: rationalize the ~9 ad-hoc small font-sizes into one Swiss ramp — “Univers Stepped”: 11 / 13 / 15 / 21 / 26 / hero, as $fs-* tokens. Every font-size now references a token (no hardcoded px/rem). /type and /photos pages updated to match.
  • Steering + AGENTS: correct the house-style description — the site is Swiss-modernist / Müller-Brockmann grid, monochrome (no accent, no per-section color), not the “Experimental Jetset + per-section accent” the doc still claimed. The $sections color map and accent are gone; documented as retired.
  • /photos page restyled to the monochrome system + type ramp (26px category heads, 13px/11px steps, hairline “no photo” box instead of the old striped gradient; drop the monospace <code>). Also fixed the root cause it exposed — the page had no layout: front matter, so it shipped as raw serif with no CSS; added layout: page and an AGENTS.md guard requiring every .html page to declare a layout.
  • Steering page: give it a proper reference layout (toc: true → two-column in _layouts/page.html): a sticky left nav auto-built from the section <h2>s (scroll-spy highlights the current section) and the body in a ~40rem reading measure. De-boxed inline code in doc bodies (faint fill, no hard border — the recipe chip was too noisy across a code-dense doc) and tightened the section-head rhythm. Collapses to one column under 46rem.
  • Header: inner pages now show a minimal left-aligned “← index” back link in column 1 instead of the right-aligned “RECIPES BY HALFCADENCE” wordmark (the home masthead is unchanged).

2026-07-21

  • Photos review page (/photos/, in Colophon): every recipe grouped by category with its hero photo or a striped “no photo” marker + per-category coverage count. Mirrors the registry resolution so it’s an accurate at-a-glance audit.
  • Recipe photos: drop the per-category fallback (no photo is better than a wrong/generic one — a tortilla on the steak page). A recipe not in _data/photos.yml (and with no front-matter image:) shows no hero. Add verified per-dish photos as they’re found (16 so far: purin, gyoza, cherry pie, matcha latte, tortilla, prime rib, canelé, old-fashioned, margarita, tonkatsu, chocolate gateau, iced pour over/aeropress, mojito, banana bread ×2). Steering + AGENTS updated to the no-fallback rule.
  • Recipe photos: add a full-width 16:9 hero photo to every recipe via a two-layer registry (_data/photos.yml) — specific: per-slug verified photos win, else a per-category fallback so no page is imageless. _layouts/page.html resolves front-matter image: → specific → category. Verified per-dish photos for purin, gyoza, cherry pie, matcha latte, tortilla; verified soups/sides/pantry fallbacks. Document the system + the find/verify procedure in steering.md (“Recipe Photos”) and AGENTS.md; clarify the “no illustrations” rule means no drawn/riso art, not no photos.
  • New article: “Photographing Food at Home” — one-window/diffuse/fill/angle/settings/styling/edit, in the marginalia-subhead article layout. Listed under Articles.
  • Analysis block restyled to the rule-free L2 (steps-pattern) layout: label · grams (small grey) · % far-right, no borders/header — was a bordered table that read as bolted-on; the ingredient-table transform was also over-matching it (and article data tables) and causing overlap. Scope fixed. Analysis now precedes Notes site-wide (reordered 14 recipes).
  • Drinks: add by-weight analysis to all 26 (cocktails: ABV/sugar/dilution; coffee/tea brews: brew ratio + grounds retention; milk drinks: composition).
  • Redesign (monochrome Müller-Brockmann grid): single typeface (Helvetica; dropped the serif accent), responsive 12/8/4 grid, home wordmark + count + cQ category heads as full-width bands, hero recipe number on the baseline, notes as 3 equal footnote columns (flush-left, no jut), black 3×3 grid favicon, colophon delisted (Colors/Type-Comparison/SVG+Riso illustrations).

  • Banana Bread: rescale the base to a full, tall 9×5 loaf (×1.3, now 3 whole eggs). Ingredients up proportionally (banana 340→442g, flours 96→125g each, etc.); recompute analysis (1232g batter — ratios unchanged: fat 13.1%, sugar 20.0%, structure 21.8%, banana ~36%). Bake time now ~55–70 min for the single tall loaf; the deep-6×3 and 6×2 rounds move to a “scaling down” note.
  • Fix broken internal cross-links site-wide. Recipe/article body links written as [text](/r/slug) rendered as root-absolute /r/slug, dropping the /recipes baseurl and 404’ing (e.g. the flan’s “Related custards” links). Wrap all 72 such links across 26 files in Jekyll’s relative_url filter ([text](/recipes/r/slug)), which prepends the baseurl → /recipes/r/slug. Index links already used relative ./ paths and were unaffected. Verified live via a real browser click (flan → Purin resolves).
  • Add a rule to steering (and AGENTS.md) requiring body-text cross-links to use relative_url rather than a bare /r/slug, so the baseurl bug doesn’t recur.
  • Flan Pâtissier: sharpen the cream note — all-milk is the classic and is rich enough (yolk fat + ~5% starch); quantify the optional cream upgrade (~100–170g / up to ~20% of the dairy, since cream fat inhibits the set past that) and keep the mascarpone variant.

2026-07-12

  • Add Bakery-Style Banana Bread recipe (Eric King / easygayoven) — the American bakery-loaf counterpart to the Yoshida base: butter and oil, granulated + brown sugar, Greek yogurt, both baking soda and powder, mixed by the muffin method (melted butter, no creaming) for a light, tender, open crumb. Analysis (1237g batter): fat 13.0%, sugar 26.7%, structure 24.7%. Notes cover measuring the banana, temp-based doneness (200–205°F), yogurt/cream/milk swaps, and why melt-and-whisk keeps it loose. SVG + riso PNG (two offset squares, blue/pink overprint).
  • Banana Bread: loosen the base toward a moister, more open crumb. Fold in 30g neutral oil, 40g heavy cream (or milk), and 4g baking powder alongside the soda; bump both flours to 96g each (192g total) for structure. Recompute analysis (936g batter: fat 9.1→12.9%, banana ~40→~36%, structure held ~22%). Rewrite the intro, the soda-only note (now soda + powder), and the keeps-poorly note to match; drop the earlier standalone “looser variation” note now that it’s the default.
  • Add Flan Pâtissier recipe — the Parisian custard tart: a cornstarch-and-egg-set vanilla custard cooked like a pastry cream (boiled to kill the yolks’ amylase), poured hot into a raw (not par-baked) pâte brisée shell, baked hot for the dark blistered top, then chilled overnight. No par-bake needed — the set custard carries little free water and the long hot bake cooks the base; keep it crisp by freezing the shell hard, pouring the custard hot, and using a metal ring (par-bake given as optional insurance). Sized for an 8×2-inch tart ring; the custard uses the one-pot cold-start method (everything whisked cold and boiled, no tempering — Stella Parks-endorsed, and even safer since the flan bakes again); a real vanilla bean can go straight into the cold pot (seeds + pod, no separate steep) since it comes to a boil anyway. Notes cover the boil-the-custard rule, chasing the dark top, and a mascarpone-enriched variant. Analysis on the custard only (crust excluded). SVG + riso PNG (pink slice, blue dark top).

2026-07-10

  • Add “Ripening Bananas” article — the ethylene/starch→sugar science, and the honest ranking of ripening methods: counter-ripen (best) and frozen-already-ripe, paper bag (~1–2 days), and the oven/microwave shortcuts that soften but barely sweeten. Busts the apple-in-the-bag myth. Linked from the Banana Bread recipe’s ripeness note.

2026-07-09

  • Add Banana Bread recipe (Morihide Yoshida) — baking-soda-leavened, pound-cake-style, ~40% ripe banana. Translated from the original (cassonade → light brown sugar; 中強力粉 medium-strong flour → all-purpose, blended 1:1 with cake flour for a tender-but-sturdy crumb), with scaling for a 6-inch round or 9×5 loaf and a serving-as-cake note. Notes cite Yoshida’s original 180°C/35min in two small shallow pans and explain the single-deep-pan adaptation (drop ~25°F, ~1.5–2× the time), plus oven-without-340°F options (325°F straight, or 350°F→325°F). SVG + riso PNG (blue loaf, pink dome).
  • Add Cherry Pie recipe — easy fresh-cherry pie in a store-bought crust. Cornstarch thickener sized for juicy fresh fruit (32g sweet / 40g sour), sweet-vs-sour sugar and acid adjustments, hot-start-on-a-preheated-sheet-pan for the bottom crust (no blind bake), the bubbling-center doneness cue, and full cool-down to set. Reduce the macerated juice on the stove before filling (a reliable anti-runny step). Corrected the whole-cherry buy weight to ~2.25 lb (10–12% pitting loss). Adds a cast-iron skillet method (Field No. 8: ~1150g cherries scaled up, assemble cold, move up a rack at 375°F; bake directly in a seasoned pan, clean and re-oil after). Analysis on the filling only (crust excluded, per French Silk Pie / Tarte Tatin precedent). SVG + riso PNG (pink pie circle, blue slice wedge).

2026-07-08

  • Add Whipped Cream recipe — the three-stage lift-and-look test (soft/medium/stiff), cold-everything rationale, over-whip rescue (fold in cold cream), and stabilizer options. Leads with the hand mixer (deep chilled bowl, start low then medium-high) as the everyday tool; immersion blender, stand mixer, whisk, food processor, and jar-shake given as alternatives. SVG + riso PNG (blue circle base, pink triangle peak).
  • Gateau au Chocolat: tune the dark version moister (it ran drier/firmer than the matcha sibling). Milk 90→100g, Dutch cocoa 18→12g (fewer liquid-absorbing solids), and add 1 extra egg yolk for fat + emulsification; flour held at 20g on purpose (it’s the binder, not a moisture lever). Recompute analysis (519g batter: structure 13.5→11.8%, liquid 40.2→42.4%). Notes: why cocoa/dark chocolate dry the crumb, the more-chocolate-punch alternative (raise cocoa + milk, or higher-fat cocoa), and why butter is a weaker moisture fat than yolk/oil.

2026-07-06

  • Purin: bump the caramel to 100g sugar (from 70g), water scaled to 20g + 20g. Gives a thicker, saucier amber layer — enough to pool nicely when unmolded, and better suited to a wide single pan (e.g. a 6-inch round) where the caramel spreads thin.

2026-07-02

  • Chocolate Mochi Canelé: fix the layer-yield mismatch — the chocolate layer is the full Ferrandi batch (~1007g, ~24 molds) but the mochi was sized for ~12, leaving a big chocolate surplus. Double the mochi (whole egg now, no weighing half an egg) so both layers finish together across 24 molds (~20g mochi + ~42g chocolate each). Yield 12 → 24; note the fuller ~21-mold option.

2026-06-30

  • Add Big-Batch Hong Kong Milk Tea Canelé and Big-Batch Thai Tea Canelé (24-mold scale-ups of the tea canelés, ×2.2 with eggs snapped to whole counts; the Thai version pushes evaporated milk to ~36% of the dairy for a fuller cooked-milk body). Both note full-tray oven handling and a condensed-milk experiment arm.
  • Add a Drafts page (/drafts, linked from Colophon) — untested recipes (the three Thai tea canelés + the Thai iced tea drink) now readable on the site, kept out of the main index until baked and proven.
  • Move the two unlisted pages (P Bass Foam Mute, Ttukbaegi Comparison) into /drafts and list them there; the ttukbaegi permalink moves from /ttukbaegi to /drafts/ttukbaegi.
  • Add README architecture map + an AGENTS.md documenting repo conventions (both build-excluded).
  • Add “Cream in Custards” article — what replacing milk with cream does to a custard/canelé: richer then greasy past a point, browns faster (bake the second stage ~15–25°F cooler), sweet spot is swapping ¼–⅓ of the milk.
  • Add “Condensed Milk” article — what it is vs evaporated milk, the DIY evaporated-milk-plus-sugar substitute, and the sugar-subtraction rule for baking with it (cut ~0.55g sugar per g condensed milk). Companion to the Evaporated Milk article.
  • Evaporated Milk article: add a “why cook milk twice?” section — the custard cook (structure) vs the can’s retort cook (caramel flavor) are different jobs; the double cook is the backbone of flan/leche-flan/tres-leches, worth it only when you want the cooked-milk note.

2026-06-29

  • Big-Batch Canelé: add non-convection-oven troubleshooting for a full 24-mold tray — preheated baking steel (the key fix), 550°F start, stop opening the oven to fight mushrooms, rotate-once-late, and the bake-12-at-a-time fallback.
  • Big-Batch Canelé: clarify start-temp logic — 550°F is a no-steel compensation; with a steel set ~475–500°F instead (don’t stack 550 + steel = scorched bottoms). And you can’t bake low-only (350–375°F) — caramelization needs a ~320–340°F surface, which a low oven can’t drive past the wet-surface 212°F plateau.

2026-06-26

  • Earl Grey and Thai tea canelés: made alcohol-free. The spirit was aromatic, not structural, so Cointreau → orange zest + bergamot extract, and rum → added vanilla + a little more tea, each replaced gram-for-gram with milk to hold the batter balance. Original boozy versions noted inline.
  • Canelé: add an all-flour (traditional, no-cornstarch) option note — the cornstarch is a deliberate tweak toward a softer crumb, not a requirement.
  • Add “Buying Bags for Canelés” article — waxed/glassine/kraft single-and-pair bags ranked by fit, cost (~$0.03–0.16/bag), and stamp-friendliness; companion to Packing Canelés.
  • Bags article: add a “Stand-up & flat-bottom bags” section — flat-bottom/SOS box bags and roll-top triangle bags that stand on a stall table, with the base-dimension sizing rule and closures.
  • Bags article: add a “Boxes & the elevated tier” section — giftable stock boxes (ballotin, rigid, drawer, window), the honest custom-print MOQ/cost reality, and the recommended middle path (stock box + riso sticker/belly-band).

2026-06-25

  • Add galbi recipe (traditional Asian-pear marinade for LA-cut short ribs, grilled over high direct heat on a gas grill).
  • Steering: correct the Nova Canvas riso-PNG profile to shiauas-personal (the old personal profile’s creds were stale).

2026-06-22

  • Add steeping tea into milk article (infusing without throwing off ratios; steep-and-measure-back; scales to any batch; what tea to buy with links).
  • Tea canelés: steep tea in milk only, then weigh and measure the strained milk back to a clean 340g base; corrected absorption estimate to an honest 2–4× range.

2026-06-07

  • Add savory canelé recipe (canelés salés — bacon, Comté, caramelized onion; sugar slashed).
  • Add evaporated milk article (what it is, baking uses, ratio-neutral substitution).
  • Add Earl Grey, Hong Kong milk tea, and Thai tea canelé recipes (tea-infused variants of the custardy canelé).
  • Add big-batch canelé recipe (custardy canelé scaled to fill 24 molds).
  • Add packing canelés article (gift-box sizing from 1 to a full batch, with sourcing links).
  • Add chocolate soufflé recipe (Robuchon’s 3-ingredient version via Chris Young, Swiss meringue, no flour).
  • Add black tea pudding recipe (water-bath baked tea custard with Calvados).
  • Rename canelés: the custardy recipe is now “Canelé” (the default), the full-flour one is “Cakey Canelé”.
  • Add cream far breton recipe (cream-rich, canelé-like, deeply caramelized; sized for the Field No. 8).
  • Add cherry buckle recipe (leavened coffee-cake batter, cherries, streusel top — the cakey counterpoint to the clafoutis family).
  • Add The Field No. 8 skillet article (cast iron care, seasoning, the acid problem, recipes sized for it).
  • Add sour cream cherry clafoutis recipe (Ishikawa — custard-end of the clafoutis family, ~3% flour).
  • Add chocolate mochi canelé recipe (hybrid: butter-mochi base + chocolate canelé top).
  • Add cherry far breton recipe (dense, sliceable cousin of clafoutis).
  • Update cherry clafoutis: swap 10% of milk for cream (tested), add analysis section.
  • Update custardy canelé: tested bake (525°F shock → 375°F 50–55min), nonstick spray works.
  • Update crispy butter mochi: hot-shock method to control ballooning, lighter-version note (baking powder, tapioca).
  • Add ingredient ratio analysis sections to all 26 baking recipes (% by weight of fat, sugar, structure, liquid, protein) with texture benchmarks — a teaching feature for learning how ratios drive texture.
  • Add .analysis styled block, batter-analysis on-page format, and steering convention for the analysis sections.
  • Add tteokbokki recipe (spicy rice cakes with anchovy-kelp stock).
  • Add kimbap recipe (seaweed rice rolls).
  • Add Eric Kim matcha latte cookies recipe (matcha cookie with ermine frosting).
  • Add Vietnamese coffee marble cookies recipe (creamed drop cookie, espresso + condensed milk swirl).
  • Add matcha shortbread recipe (slice-and-bake, low-temp to keep the color bright).
  • Add custardy canelé recipe (half batch ~12, lower flour + cornstarch for a silkier set).
  • Add crispy butter mochi recipe (Shanghai-style, baked in canelé molds).
  • Add cherry clafoutis recipe (sized for the Field No. 8 skillet).
  • Add eggnog canelé recipe (nutmeg, cinnamon, clove, bourbon).
  • Add white chocolate matcha canelé Ferrandi recipe (higher flour to compensate for white chocolate’s lack of cocoa solids).
  • Add spirits in baking article (pairing matrix, spirit decomposition, test results).
  • Add batter analysis skill (fat/sugar/structure/liquid/protein decomposition with benchmarks).
  • Update chocolate canelé Ferrandi: increased flour, tested bake schedule (450°F/10min + 375°F/50min), cover technique, Dutch cocoa substitution note.
  • Update chocolate canelés: default to brandy/cognac (tested better than rum).
  • Add salt to all canelé recipes (Marchal ratio).

2026-06-01

  • Add miyeokguk (Korean seaweed soup) recipe.
  • Add Japanese beef curry (lazy frozen veg version) recipe.
  • Add cucumber tea sandwiches, watercress tea sandwiches, ham & cheese tea sandwiches recipes.
  • Add tamago sando (Japanese egg salad sandwich) recipe.
  • Add ankake don (thick dashi sauce rice bowl) recipe.
  • Add ume highball recipe.
  • Add chocolate canelé recipe (Marchal-adapted).
  • Add chocolate canelé Ferrandi recipe (low-flour, intense chocolate).
  • Add white chocolate matcha canelé recipe (experimental).
  • Add canelé ratio analysis article.
  • Add ttukbaegi comparison page (unlisted).
  • Add shopping list generator skill.
  • Update canelé recipe: simplified method, AP flour, tested bake schedule (475°F/11min + 350°F/50min), flavor notes (matcha, hojicha, coffee).
  • Update nicoise salad: fresh seared tuna, jammy eggs, remove tomatoes.
  • Update gamjatang: siraegi/yeolmu prep instructions, watercress note.
  • Update samgyetang: donabe serving note.
  • Update potato salad: sando filling note.

2026-04-29

  • Add AeroPress filters article (paper vs metal vs cloth, flavor profiles, stacking technique).
  • Add coffee syrup pantry recipe.
  • Add gamjatang (pork neck bone soup) recipe.
  • Add samgyetang (ginseng chicken soup) and yukgaejang (spicy beef soup) recipes.
  • Add reheating frozen bread article.

2026-04-23

  • Add segmented color bar header on unthemed pages (all 8 section colors), solid on themed.
  • SVG illustrations gallery now auto-discovers files with shape descriptions from _data/illustrations.yml.
  • Add new recipe checklist to steering.
  • Remove non-actionable notes across 7 recipes. Add actionability rule to steering.
  • Add iced AeroPress, strawberry milk, French silk pie, simmered potato and chicken recipes.
  • Add 76 AI-generated riso illustrations via Amazon Nova Canvas. New /illustrations-riso/ gallery page.
  • Add illustration generation rules to steering (SVG + riso PNG pipeline).
  • Riso illustrations used as centered footer stamp on recipe pages (PNG preferred, SVG fallback).
  • Minimal 2-shape SVG illustrations for all recipes with self-documenting gallery.
  • Same-color overprint technique via paper background rect.
  • Per-section colored favicons. Custom <title> with · separator.
  • Favicon, OG image, 404 page, pill hover, RSS cleanup, sitemap.
  • Selection color per section theme.
  • Paper background #fbfaf8. Bump base font to 1.0625rem. A11y contrast fixes. Focus-visible outlines.
  • Thicker 1rem top border. Print stylesheet.
  • Misregistered overprint instagram pill on about page.
  • Switch active palette to Blue + Fluorescent Pink overprint. Previous spot-color palette to first alt.
  • Move overprint palettes from riso article to colors page. Consolidate color catalog into single table.
  • Rename Port Tonic, Suze Tonic. Restore Acid-Adjusted OJ title.
  • Add section breaks to homepage index with toggleable block layout.

2026-04-21

  • Add 11 overprint palette explorations to riso article (1–3 base inks, overprints, tints, split-color treatments).
  • Make riso article swatch pills full-width in their table cells.
  • Expand riso color catalog from 12 to 77 colors in the risograph article, organized by hue family.
  • Add Riso Overprint palette (Blue + Fluorescent Pink two-ink constraint) to colors page.
  • Add shaken black tea (Taiwanese 泡沫紅茶).
  • Add 3 coffee recipes: pour over (Tetsu Kasuya 4:6 method), iced pour over (Japanese flash brew), Hario Switch, AeroPress — with grind settings for Remi and Lagom Mini 2.
  • Add grind settings to coffee manhattan.
  • Correct grinder settings to official units: Lagom Mini 2 uses dots from zero, Remi uses revolutions.
  • Add 2 articles: Dialing In the Remi, Dialing In the Lagom Mini 2.
  • Rename “Style Guide” section to “Colophon”.
  • Add changelog page with full reverse-engineered history. Add changelog rule to steering.
  • List base recipes before variants in index.
  • Distinguish technique modifiers from flavor modifiers in naming rules. Add 7 recipes: steak au poivre, corn soup, kinpira gobo, ohitashi, potato salad, highball, yuzu highball.
  • Add riso color rules to steering.
  • Distinguish technique modifiers from flavor modifiers in naming rules.
  • List base recipes before variants in index (highball → yuzu highball, gateau au chocolat → matcha/orange/hojicha/vanilla, brownies → matcha brownies).
  • Rename Chocolate Gateau au Chocolat → Gateau au Chocolat.
  • Rename Porto Tonico → Port Tonic, Suze & Tonic → Suze Tonic.
  • Simplify 27 recipe and article titles: drop technique modifiers, parenthetical descriptions, chef names, article subtitles.
  • Add naming rules to steering.
  • Riso-derived dark mode: all grays derived from Midnight and Light Gray inks. Themed pages keep section colors in dark mode. Articles section uses lighter Midnight variant.
  • Fix notes heading/body color mismatch in dark mode.
  • Note dividers use neutral grey instead of theme color.
  • Riso article color table uses pill-style hex swatches with colored backgrounds.
  • Fix hardcoded grays in type-comparison and colors pages for dark mode.
  • Body text uses Midnight (#435060) in light mode instead of minima default.
  • Add steering page (published style guide). Kiro steering doc references it as source of truth.
  • Drop numbers from style guide and article pages.
  • Allow new categories in steering doc.

2026-04-20

  • Switch to authentic Risograph ink colors from stencil.wiki.
  • Add article: Risograph Printing and Color.
  • Add Style Guide section to homepage (colors, typography, type comparison).
  • Make section labels clickable — navigate to homepage anchor.
  • Add recipe numbers (#001–#063) to all recipes.
  • Hang step counters in left margin as monospace notes.
  • Switch notes to tight vertical stack with thin separators.
  • Apply inverted pill style to index section titles with auto-numbered counters.
  • Convert all px values to relative units (rem/em).

2026-04-19

  • Add gyoza, matcha brownies, chocolate brownies, Robuchon mashed potatoes, mentsuyu, ponzu.
  • Add coffee manhattan, porto tonico, suze & tonic, lillet spritz.
  • Add gimlet, manhattan, old fashioned.
  • Add tonjiru, sukiyaki, ishikari nabe, tonyu nabe.
  • Add awase dashi, ushiojiru, kombu dashi, clam miso soup.
  • Add salade niçoise, nitsuke, chawanmushi.
  • Add tarte tatin, purin.
  • Add acid-adjusting juice article.
  • Add whiskey sour, acid-adjusted OJ variant, tonkatsu.
  • Add tortilla española and lazy version.
  • Add karaage, prime rib, thin burgers.
  • Reorganize index from 11 categories to 7.

2026-04-18

  • Add 6 recipes: pickled cabbage, pickled daikon, margarita, mojito, corpse reviver no. 2, tinto de verano.
  • Standardize all recipes to table format and grams.
  • Use oz for cocktail measurements.
  • Use Bricolage Grotesque font.
  • Brutalist typography: uniform text size, bold headings, inverted pill titles.
  • Brutalist header: border-top, no nav, “recipes by andrew”.
  • Remove footer. Combine title and attribution in header.
  • Add dark mode support.
  • Convert all temperatures to Fahrenheit.
  • Unify formatting across all recipes.

2026-04-17

  • Add brown butter hojicha white chocolate cookies.
  • Add orange chocolate and hojicha gateau au chocolat.
  • Add vanilla bean gateau au chocolat with brown butter.
  • Add matcha americano and matcha latte.
  • Add article: salt percentages in baking.
  • Bump salt across gateau and cookie recipes.

2026-04-16

  • Add chocolate gateau au chocolat.
  • Add earl grey chiffon cake, matcha chiffon cake, matcha gateau au chocolat.
  • Convert all recipes to metric measurements.
  • Add article: hydration and baking ratios.

2026-04-15

  • Add canelé recipe (Gilles Marchal).
  • Add miso grilled chicken and miso marinated steak.
  • Add matcha white chocolate cookies.
  • Add double chocolate chip cookies.

Earlier

  • Initial site setup with Jekyll and minima theme.
  • Add matcha affogato, coffee jelly, tea jelly, bay leaf panna cotta.
  • Add cold brew tea, thyme streusel, salted caramel sauce.
  • Add about page.