Lemon SqueezyHow it works

The cooking screen

One screen and three promises. It shows where you are, what is next, and no figure the recipe did not give.

The ring

The recipe graph drawn as concentric arcs sweeping to a plate, one arc per workstream, generated at render from the graph and the fold — never authored per recipe.

From DESIGN.md, Signature components

Nothing may imply a fraction of a duration. The route is drawn leg by leg, one leg per step, at constant width — never a fill, never a percentage. Progress is a count of steps, because the total is the longest remaining path and the fold cannot know it.

From DESIGN.md, The ring grammar
The cooking screen on a phone as a cook of Bucatini all’Amatriciana opens: the ring of the recipe’s lines above, the first step to do in yellow with its Done button, and the steps after it below.
The cook as it opens, nothing done. Photographed at commit 22bf12747, 25 October 2026.

Signature components

DESIGN.md, the whole section
  • The ring. The recipe graph drawn as concentric arcs sweeping to a plate, one arc per workstream, generated at render from the graph and the fold — never authored per recipe. It is the carrier of "where am I", and its rules are the ring grammar. It is a ring at any number of strands: past five it squishes further and grows taller rather than turning into another drawing (owner, 17 October 2026, who was shown lanes above five and picked the ring held), so a recipe cannot draw one way at the stove and another on the summary. Past five its names are also set by a stricter rule (ring-label-crowded.ts, ring-label-path.ts, ring-crowded.test.ts): none over any bead, a gap between any two, each whole, and none laid flat: a name follows its arc up to vertical and reads from bottom to top on either flank (owner, 21 October 2026, from the choices drawn on the six- and seven-strand maps, because a flat name among curved ones read as random). On the right flank, where the arc falls, it is written up a short path of its own. A ring of five or fewer is placed as it always was. The strand count is the recipe's, not the hob's. The Wellington runs a water bath, an oven, a fridge and a defrosting counter and no burner; the roast dinner has five strands, two on a burner. The drawing never changes shape for the count.
  • The step card. The ingredients ruled like a notebook first, amount in its own column; under them the step in the recipe's own sentence, set in Inter because it is an instruction; last the status line with the fixed action slot beside it — and at the line's end either the note's own control or, where Details holds more than the note, the way into Details. No bead: the surface, the lane's edge and the status word say all a mark said, and its column was 28px the sentence now has. The amount is large because the ingredient is the part of a step you actually need at the stove, and the sentence is the whole method because a headline that had dropped its figures made the cook open every card to find them (SCREENS §3).
  • The group. A bracket drawn around the steps that are genuinely simultaneous, labelled "At the same time", and one scroll-snap target — so the cook meets concurrent work as one thing to read.
  • The pill and the tick. The app's two small controls, and between them every "is this on?" it asks. A pill is the shape — .pill, one rule — worn by the rating answers, the measure switcher (on the grocery list one of its three is the app's own lemon, 1.45em in full colour whether chosen or not, the wash behind it saying which), the grocery list's three readings; a tick is .box, drawn by TickBox (components/tick-box.tsx) and worn by the grocery line, by Tick, the checkbox component, and by each row of PickSeveral, the pick-menu that ticks any number from a list (components/pick-several.tsx; PickMenu picks one). A pill is for a bounded choice you switch between over and over; one you set once and leave, or one whose count is the owner's business, takes a select instead — which is why the shopping list's readings are pills and its order is not (see SCREENS.md §1). On is the yellow, in both, whatever drives it: aria-pressed, aria-checked, a class, or a real :checked. Four triggers, one look, one place it is declared. The two checkboxes were the reason. Left native and merely given an accent-color, they rendered as near-black squares — one of them directly beside the cream-and-yellow rating pills on the result card, so a single card spoke two design languages and the louder was not ours. Tick keeps a real <input type="checkbox">, focusable and announced, and only takes over the paint. No control rests on white. A control at rest, which is every pill, tick and outlined button, and a switch's knob, is warm paper: --control (the card's own off-white, never --surface-lowest) under --sh-paper, the picker tiles' soft fall, edged in --control-edge, which is nothing. A surface made of the same paper (the cook screen, home's tonight card) sets --control-edge to the faint --outline-variant so its controls are not lost on it, and the tick keeps that edge itself, being too small for a fall to show. The on state is still the yellow. A one-line field is the exception, below; so is the Google sign-in button, which keeps Google's own white face because Google's branding rules require it (owner, 13 October 2026).
  • The one-line field. Anything a cook types one line into (the sign-in email, first run's name, a kitchen's or a list's name, the import link, the recipe search, a night's note, an ingredient's note, the stove's timer minutes, a grocery line's name, amount and note, the grocery add line) is .field in globals.css: a box on --control under --sh-paper with a 1px --outline edge all round, in --r-inner (12px) corners (owner, 13 October 2026, option A, a boxed edge; 12px everywhere on 21 October, so not the pill it first was, and one radius for every box you type in or pick into). The edge is --outline and not --control-edge because a field has to be found: --outline is 4.06:1 on the page, past the 3:1 WCAG 1.4.11 asks, and the faint edge a resting control wears is not; the owner's AA commitment of 5 October is why, and app/field-edges.test.ts holds it. It is an <input> itself, or a wrapper (.field--lead) holding a mark and a .field__input; the search field is that wrapper written once, as the SearchField component (the glass, the hidden label and the phone's typing hints), and every search in the app is one, never the markup again; .field--snug is the same box at the tap floor, for a row that shares its width with a button or a card with no height to spare (an ingredient's note, the stove's minutes and a grocery line's three all wear it). A screen sets only how it sits (width, flex, a tighter padding), never the box again. The list line, .field--line, is the one variant, and only the grocery list's add line wears it (option D): square corners and a 1px --outline rule under it only, on the same paper and fall. It is a line on a list you write down, typed into in passing, so it reads as part of the list; every other field is filled once and submitted, and reads as a form box. Not this: the guided editor's underline, the open version name, and a select (a grocery line's aisle keeps its own box until the pick menu redraws it, edged in --outline like the rest). A many-line box is .field-area, the field's sibling in globals.css: the stove's note on a step, the evening's note, the pasted recipe and the assistant's question are one box, warm paper under --sh-paper in --r-inner corners at --fs-field, not resizable, edged in --outline like the one-line field (owner, 20 October 2026, from drawings of both: it had been edged in --control-edge, which is nothing on the page and so left the box unfound). A screen sets only where it sits (a margin, a height to give back). Not the recipe page's note, which is borderless until it is written in. A pick menu's button standing as a field is that box too (PickMenu's field; owner, 21 October 2026, "12px everywhere"): Settings' Home kitchen and Language and the editor's unit picker take .field's edge, corners, paper, fall and 16px, Inter at 400 in --on-surface, and a chevron at the end, from the one rule in globals.css (.pickmenu__button--field, listed with .field), and a screen sets only how wide it is. A pick button that is a word (the shelf's sort, the version menu, the kitchen line, the grocery list's title) is not a field and draws no box.
  • The quiet button. The second control beside the one yellow thing (the stove's Undo and Add note, the timer panel's Start, Stop, Clear and Copy, the recap's and the share's Undo) is .quiet-button in globals.css: warm paper under --sh-paper, --tap tall, --r-action, 14px at 600, the receding ink, and no edge whatever the ground, so the fall alone shows it. A screen sets only where it stands (the stove's 86px slot, the panel's 72px floor), never the box again, and the recap's and the share's Undo are one component, ResultUndo. The timer panel's four are the one exception to --tap tall: 34px to look at, with a --tap target from .tap-reach (owner, 19 October 2026: at 44px they filled their rows flush to the hairlines and crowded the minutes field).
  • The notebook card. How a group of things, or a choice from a few, is drawn: one warm card on the page's own paper (--surface) under --sh-paper, its lines ruled like a notebook, each field's name in a narrow left margin in the small label (.micro, in the brown) and its value on the line in the serif. In a choice the chosen line has the yellow tick in the margin and the light yellow wash across it. Owner, 12 October 2026, picking direction B of three over heavy cream wells ("too dull and heavy") and over a variant matched to the ingredient cards ("stick with B"). It is the Details tab's source and picker rows, the evening's three ways with Kids to mind, and the recap's questions, and not Settings or the kitchen card, which keep their own cards. components/notebook is its one home (Notebook, NotebookRow); a screen sets --nb-margin for a narrower margin (the evening's holds only a tick), --nb-row and --nb-pad for shorter rows where it must fit a short phone (Details', under 760px tall), and never draws its own ruled card. Its controls rest on --control, so they stand out from the card and are never white.
  • The choice track. A choice among a few whose parts run in an order (Light, This phone, Dark; Small, Default, Large): one rounded warm-paper bar the card's width, equal parts until a word needs more room (it never wraps or shrinks; the others share what is left), the chosen one filled yellow, every part a tap high, a radio group the arrows move through, named by the card's own heading (labelledBy) and never by a second string. A pill stays for a set with no order (the foods you leave out) and a pick-from-a-list (PickMenu) for one that grows (Language). components/choice-track.tsx is its one home.
  • The pick menu. The one way to pick from a list, in the old dress (owner, 15 October 2026, over the cream box of 12 October): a button with the pick's name and a chevron that turns up while it is open, and a white box (--surface-lowest) on --sh-md with a 1px --outline-variant edge and 12px corners, hung 0.1rem under it. Rows are a tap high with 12px corners and no rules between; names in the serif at 1.05rem in --primary, a day or count under one in small grey. The row showing is said by a tick in a 1rem gutter every row reserves (--on-surface-variant), never a yellow wash, so no name moves when the pick does; the tick-box menus (PickSeveral, the Filter and the books) have no gutter, their box is the tick. An optional heading is one line of small capitals over the rows, indented to the names (the kitchen menu's KITCHEN); a foot is a band under a soft rule inset 0.3rem at each end (New list, Manage). The one variant is plain, for a short list of plain words (the editor's units): Inter 0.95rem rows with a --card-edge rule between, every row showing before the box scrolls. A box that shows every row (plain, and the tick-box menus) is as tall as its rows up to a cap and never taller than the room under its button: usePickBox measures that from the rows' top to the foot of the page body, which is the nav's top, and writes it as --pick-room, so on a short phone the box ends above the nav and its last rows are reached by scrolling it. Every pick menu shuts on Escape, on a tap or focus outside it and before a pick acts; Up, Down, Home and End walk the rows, and opening puts focus on the row showing. A screen never draws a second menu: the top bar's kitchen menu, the editor's unit menu and the grocery list's picker are this one. components/pick-menu.tsx and pick-menu.css are its one home.
  • The ask. The one question before an act that cannot be undone (owner, 22 October 2026, of the recipe page's Delete: "codify this confirmation modal and make sure it's used everywhere"). It stands in place of what opened it and never over it (a modal to collect one answer is ceremony, and on a phone it hides the thing being asked about), as a named group: the thing in a question at reading size, what goes with it in plain short sentences (the sentence that says what the act does at full size, what it costs and what stays quieter), then the act, outlined, and the way out, a bare word, last — so the safe answer is never the one dressed to be tapped, and a held Enter lands on it. A further way out (a link to where the typing is) goes between the two, never after. With no question it is the row (owner, 13 October 2026, "the buttons state their actions", on the lists, the plan and the kitchen's people): the same two buttons, the act's accessible name carrying what it acts on. Where it stands decides a look (the name's menu, the kitchen's card, settings' card, the lead's seat, the row) and a look picks class names and nothing else. Not asks: a choice with no safe answer (a twist or a tweak), the assistant's consent (calm on purpose, neither answer destructive) and a refusal with an OK. components/confirm-ask.tsx is its one home; a screen words its own ask and never draws one, and hand-drawn-ask.test.ts fails the build on one that does.
  • The zest check. A warm 45° texture at half strength. It anchors the white cards to a linen surface; if you notice it as a pattern, it is too strong.
  • The wait. What a screen shows while it has nothing to show yet: a simmer for a transition, bones shaped like what is coming for a data read. It appears on every screen, so it is a component rather than a screen — its rules are the wait.

The ring grammar

DESIGN.md, the whole section

The map's marks and the deck's states, each with exactly one meaning — because "honesty is in the geometry" only works if the geometry can't equivocate. The reference that fixes these is design/cooking-ring.mjs.

design/check.mjs asserts the ones a static deck can carry — the banned status words, the missing button on a future step, the identical action slot, one named arc per workstream at each of one to four, the plate, the group rules, and that nothing animates. It does not assert the four that need either a running app or a second recipe to compare against: that a leg is drawn per step rather than as a fill, that a leg's stroke says its state, that a blocked strand starts at the angle it was freed at, and that prep arrives as a step rather than a phase. Two are tools/drive.mjs's now — a leg counted per step, and the prep found in the list rather than before it — and two are still nobody's: the stroke a leg wears, and the angle a freed strand starts at. Saying which is which matters more than the count: a rule nobody checks is a rule that drifts, and pretending otherwise is how the yellow ban died.

  • Nothing may imply a fraction of a duration. The route is drawn leg by leg, one leg per step, at constant width — never a fill, never a percentage. Progress is a count of steps, because the total is the longest remaining path and the fold cannot know it. This is the rule the previous design broke, and it is the reason the arcs are discrete.

    The ban is on encoding an unmeasured duration, not on length itself. During a cook the future is unmeasured, so the ring's legs are constant width. Afterwards it is not: a finished cook holds a recorded completedAt for every step, so a length between two of them would be a fact rather than a guess. The summary drew one for a while, on a horizontal clock rule; it went for reasons of its own (see SCREENS.md §4), not because it broke this rule — and the arrival sentence it left behind is that same subtraction, said in words. The test is not "is it a length" but "can the fold produce both ends of it".

  • A line is laid out in the order the cook will travel it, so the colour only ever advances. The segments of a strand are its steps in cookOrderOf order — the same schedule the list beside the map runs on — never the graph's, which is the order they finish in. Where the two disagree the fill lights up partway round the arc and then comes back for an earlier segment, and a map that jumps is worse than no map: it is confidently wrong about the one thing it exists to say. This is the complaint that shaped the whole screen ("the states jump around"), answered in round two for the list and, for a while, never for the map. Asserted by replaying a cook a step at a time and measuring every line's fill, in views/example-maps.test.ts.

  • No remaining-time estimates. We cannot know when a pot actually went on, so a step shows the recipe's own committed range or nothing at all. "8–13 min left" is a number nobody measured.

  • One ink per workstream, and it never means anything else. Five inks, and above five strands they cycle — the one place two strands share a colour. Every strand is named in words on its own line, so the ink was never the only thing telling them apart — by the shortest form still unmistakable in this recipe ("Bucatini", not "Drained bucatini"), the same rule the ingredient rows use. A lane's ink is its identity everywhere it appears: the arc, the bead, the card's left edge, the status word on a running step. Legs not yet travelled draw in the lane's own ink at reduced opacity, never anonymous grey: a strand you haven't started is still that strand.

  • A leg's state is its stroke. Solid is travelled; dashed with butt caps is in progress (round caps swell the dashes into blobs); quiet is still to come. A lane that waits on another begins at the angle it was freed at, with a dashed hop outward — never running backwards in time.

  • On a group cook a live bead wears its hand's initial, and the yellow says which one is yours. Every live bead, yours included — a letter on theirs in their arc's ink, yours on the yellow — and a finished bead stays a plain tick whoever made it. On a closed map another hand's arc is a hairline in the outline ink with no bead and no name: the shape stays, so the ring is still the recipe, and where they are is theirs to know. Owner rulings, 16 September 2026; the deal behind it is KITCHENS.md's.

  • Strands that meet, meet on the map. A strand the joined run consumes curves into it — an on-ramp tangent to both arcs — at the joining step that folds it in, and ends there; the joined run carries on as one line to the plate, a bead per joining step, wearing the neutral ink (it is every strand at once, so no single strand's) and named for what the join makes ("Cookie dough"), read from the graph. Its name sits on the stretch past the last merge, where the line is finally alone — among the arriving ramps it reads as the strand crossing it. Two arcs riding independently to the plate after their mixtures became one dough was the drawing being wrong about the kitchen.

  • The half circle is dealt by steps, so every bead on the line sits one gap from the next. The strands take the share of the sweep that their host strand's steps are of all the steps on that line (35% at least, 70% at most, so a name has an arc to ride and a run keeps room), and the run's steps are spread evenly from the first join to the plate. Owner, 19 October 2026: the chicken's thirteen-bead run had sat 14.5px apart, where the current step's ring touches its neighbour under 20, and is 21px now. It counts steps and reads no figure of time, so it is ordering and the rule above stands (ring-sweep.ts). Past five strands the old flat 55% and packed folds stay until the lanes drawing redeals them.

  • A strand keeps its own ink right up to the meeting. The neutral belongs to the joined line and starts at the join, not at the host's last step — handing over early painted the whole approach in it, and "why do the lines turn grey prior to joining?" is exactly what that looks like.

  • One step, one leg, and no step may paint two. A run-up to the meeting, an on-ramp and the arrival at the plate are drawn as the tail of the last leg there was, never as legs of their own. As legs they took the state of the step they ran into, so finishing one joining step filled a segment on every arc that fed it — "the last step completes two segments, and it should fill only the end one". Finishing a strand fills its arc to the meeting; the joined line lights only when the joining step is done. Counted rather than argued, per tap, in ring-fill.test.ts and in the drive.

  • No stream may cross another stream. They meet, at an end, and otherwise they are simply labelled. Two lines that cross have swapped places in the reader's eye, which costs the map the one thing it claims: that a cook can see which strand is which at a glance. Concentric arcs cannot cross, so this is a rule about the joins — arcs are dealt alternately outward and inward, closing on the middle, so the joined run rides mid-ring, the latest joiner (or one that never joins) sits outermost, and on each side of the line nearer means joins earlier. A ramp curving in therefore only ever passes radii whose strands have already merged and have no line left at that angle (ring-order.ts). It is measured rather than eyeballed: path-crossings.ts intersects the rendered path data, at every strand count both maps draw. No gate could see this before — design:check asserts no overlap of boxes, and two curved paths crossing overlap nothing box-shaped.

  • As strands merge, the survivors close up — one ring per merge on their own side, all moving at the same angle, so every gap between live arcs stays exactly one pitch. Whatever joins next is then always one ring from the line rather than diving across the map. A strand whose own join follows straight on a freed ring does not land on it at all: the step fuses into its on-ramp, one curve from wherever it rides down to the line, so the freed space closes in smooth wedges rather than terraces (ring-geometry.ts).

  • When the strands outgrow the half, the ring squishes rather than clips. Past four strands the outermost circle is wider than half the map; the arcs become ellipses — full height, narrower — sized so every strand begins on the baseline, and the centre slides right by what the right half no longer needs: by there the strands have collapsed, so only the line still arriving at the plate claims room. Past five the map also grows a pitch a strand and squishes further, so the innermost arc keeps the radius it has at five and its name still has an arc to ride; six and seven hold every name whole, none over another and none misnamed (ring-crowded.test.ts), and no shipped recipe has either. A map that fits keeps the exact circle it shipped with.

  • A name follows its arc, and where it cannot, the drawing says so rather than hiding it. Two ways it cannot, one home for both (ring-label-fit.ts). Where every position on the arc runs steeper than 45° the name is set flat, because a name rotated past that is unreadable at the glance it gets — and being unable to curve is priced into the search, so flat is a last resort and never a name lying horizontal among curved siblings. (Past five strands it is not even that: no name is laid flat, see the ring.) Where the name is longer than the arc it rides, one rule, in this order: ride further out, where the same arc is longer (the search prices a name with no room, so a clear spot inside a short arc loses to a grazed one outside it); else wear a shorter form of the strand's own words (shorterIngredientNames: "Fried whole spices" is "Whole spices"); only then cut with an ellipsis, because past the end of a textPath nothing is drawn at all and the alternative is a name silently losing its tail mid-word. The limit is the arc's own length, never a character count — it moves with the radius and with where the strand joins. No recipe squeezy ships reaches the ellipsis (asserted over the whole shelf in example-maps.test.ts), nor does a stored share (a-strand-longer-than-its-arc.test.ts).

  • A name is nearer the line it names than any other, and that is a floor rather than a preference. A name that sits closer to a neighbouring strand is read as that strand's, which breaks the map's one promise exactly as a crossing does. The label solver scores candidates against obstacles — a foreign bead, another name, a position too steep to curve along — and every one of those weights answers what is in the way while none answers whose name this is, so a spot clear of obstacles and closer to the neighbour won. A ground that fails ownership is now excluded, not priced (label-ownership.ts).

    Measured on the ink, not the anchor, and that distinction is what makes the rule bite: text grows upward from its baseline, so a name lifted 14 above its own arc reaches ten further towards whatever is on the other side. At the old arc-to-arc pitch of 30 that left 10 of air below the name and 5.5 above it — every inner strand's name tucked under the arc outside it, which is what "why is the label for dressed pasta closer to the arc for veggies?" looks like. The pitch is therefore set by the names rather than by the lines: wide enough that a name at its first lift is nearer its own arc, asserted in ring-order.test.ts so it cannot quietly be tightened back.

  • A name keeps out from under the unit switcher. Cups and Grams stand over the cooking screen's top-left corner, where the outermost arc peaks and its name rides, so the solver prices a name there like a name over a name and looks between its usual positions before it settles for one that shares only the tap box's edge (SWITCHER_CORNER, ring-frame.ts). The deck holds that no pill sits over a name.

  • A job that feeds the join without riding a strand hangs off the ring: a mark on a hairline stalk just outside the line, never a segment of it. Grating the pecorino is on no strand's way to anywhere, and drawn along the line it claimed the dish had travelled that far — a black done-stripe across a sauce that had not met its pasta. A single bench step met after the first join is such a job, and hangs where it is used, not at the head: the oven by the roast, the yogurt by the spoon, the dill by the scatter, so the joined run begins at the first join. One met at the first join (the dry bowl, the marinade) is a beginning and keeps its arc. Its bead and its card wear the ink of the line the stalk hangs from: the host strand's before the first join, the joined run's green after it. A card's many-colour edge belongs only to the step where strands arrive, and carries only those that do (owner, 19 October 2026: "the line's colour").

  • The brief's ring shows the lines, not the steps. Its dormant ring draws arcs, joins and names with no beads at all — beads belong to the cooking screen, where each mark has a card to answer to. Label placement is identical in both, because the solver never looks at state.

  • Green now carries three meanings, and each gets its own shade. --plate and --joined are the map's green, and both mean arrival — the strands landed, not that anything is good — spanning only the lines that actually arrive, which after a join is the joined run and any strand nothing consumed. The third, --done-soft/--done-ink, is off the map entirely: an ingredient row's act, once tapped, answers in it (see SCREENS.md §13) and means recorded — a line landed on the shopping list, or a tweak was noted for next time — never arrival, and it never rides a lane or a bead.

  • Yellow means live — either it is cooking without you or your hands are on it. Everything else is history or not yours yet. Live cards carry the yellow surface and the button; a future step carries no button at all, because offering an action you cannot take is the same lie as locking one. The one exception is a step the stove holds back, which can be done: it keeps the future card's surface and takes Done in the quiet control Undo wears, never the yellow.

  • Three statuses, and only three: Done, In progress, Todo. "Locked" and "not yet" are both banned — a step you cannot start yet is simply further down the list, which is what the order was already saying.

  • A step occupies identical space in every state. Height is a function of content, never of state, and the action slot is the same box whether or not it holds a button. The list must not move under a wet thumb. A mark on a card is a state, so a step carrying a note is the same box as one that is not — measured, because the way this breaks is invisible: two inline sizes on one baseline make a line box taller than either of them, and the first draft of the mark grew the card by 1.5px.

  • "At the same time" means genuinely simultaneous — everything cooking without you, plus the one job in your hands, and only where they already sit next to each other in the list's fixed order. Steps that merely share a graph depth are not simultaneous, and grouping them was the single most misleading thing the earlier design did. The bracket never re-sorts: the first version put passives first inside it, and that reordering is what made the list jump under a cook's thumb. "Set them going first" is carried by the statuses instead — a runnable pot is already In progress.

  • Prep is not a phase. It is a step at the head of the lane that needs it, carrying the ingredient in the form you buy it — a cup of diced tomatoes is not what you brought home.

  • No decorative animation. The wait breathes in opacity and a leaving thing fades (both below); a kitchen screen is glanced at, not watched.

The list is the schedule

The yellow is the next open attended line, and the glide after a completion only goes down.

From STATUS.md, The list is the schedule

Three statuses, and only three: Done, In progress, Todo. "Locked" and "not yet" are both banned — a step you cannot start yet is simply further down the list, which is what the order was already saying.

From DESIGN.md, The ring grammar

Height is a function of content, never of state, and the action slot is the same box whether or not it holds a button. The list must not move under a wet thumb.

From DESIGN.md, The ring grammar
The same cook after three steps are done: three ticks on the ring, and the yellow card moved on to the next step to do, with the steps after it below.
The same cook, three steps done. Photographed at commit 22bf12747, 25 October 2026.

The list is the schedule

STATUS.md, the whole section

cookOrderOf orders steps by when the plan would start them: earliest start under one pair of hands, unattended first among ties, work nothing waits for behind work something does, longest lead next. The yellow is the next open attended line, and the glide after a completion only goes down. ringShapeOf lays lines out in the same order, so a colour fills forward only. The drive asserts every other card keeps its size and position across a tap. SCREENS §3.

Never invent a number

A figure is the recipe’s, or it does not exist — no number here is ever widened, narrowed or rounded

From COOKING.md, What assumed means

No remaining-time estimates. We cannot know when a pot actually went on, so a step shows the recipe’s own committed range or nothing at all.

From DESIGN.md, The ring grammar

five untimed steps once collapsed the Amatriciana brief’s stated total from a checked 29 minutes to "about 8 min", a confident wrong figure in the one place a cook plans an evening from.

From COOKING.md, What assumed means

What assumed means

COOKING.md, the whole section

A DurationEstimate whose assumed flag is true carries a number that is squeezy's, not the recipe's — the owner's amendment to "never invent a number" (CLAUDE.md), reached because the rule as first written could not survive an importer. Every example here was hand-authored with a time in it, so the gap never showed until a real page hit "cook until golden" — five untimed steps once collapsed the Amatriciana brief's stated total from a checked 29 minutes to "about 8 min", a confident wrong figure in the one place a cook plans an evening from.

A figure is the recipe's, or it does not exist — no number here is ever widened, narrowed or rounded (the one reading that rounds is a card's sum of the steps, below; it never touches a figure). What assumed marks is the one case that is not that: a step whose duration is a condition ("until the fat has rendered") with no figure in it at all, or whose condition the import left in completionCondition with no duration (on a step the cook does not stand at: a hands-on step's window is counted where the time is added up and stays out of the schedule). Rather than leave the schedule nothing to place the step by, squeezy may supply a range, never a point — "20 to 30 min" says what it is, an expectation with room in it, where "25 min" would claim a precision nobody has. assumedDurationRange (libs/core/src/import/source-figures.ts) is the one place a range is built, from the step's own verb, the food it names (and, where a row turns on how the food was bought, the ingredient lines the step is made from) and the condition's own words ("until fragrant"); where the verb is not one it has evidence for, it supplies nothing, on the same reasoning as an unparsed duration — an absence a cook can notice beats a number that looks like a good one. It supplies nothing, too, where the condition hands the end of the wait to the step that wants its result ("leave the chicken to marinate until needed"): that wait has no length of its own, and a guessed half hour the schedule took for one held the korma chicken behind the blend. endsWhenWanted (libs/core/src/import/ends-when-wanted.ts) is the table of those words, and a recipe that already carries such a window is read as if it did not. IMPORT-PIPELINE.md owns where in the pipeline this runs.

A step that handles each piece is the second thing assumed marks. The owner, 21 October 2026: "some recipes like meatballs or chicken nuggets where each piece needs to be handled individually take longer and thats not taken into account." Shaping 24 meatballs is not shaping 8, and a page states the count and never the time. The technique table (libs/core/src/knowledge/techniques.ts) carries a range of seconds for each piece an act handles (perPiece, the reason beside each row), and the step's range is that rate times the count, assumed and a range like any other, with no sourceText, since the page said nothing of the time and "into 24 balls" is not the step's time. The rate is squeezy's reckoning, not a published figure, and the count is the recipe's: piecesHandled (libs/core/src/import/pieces-handled.ts) reads it from the step's own words, then from the first food the step names when its line is a whole number with no measure, then from the page's yield for a step that handles several, and from nowhere else: no count is ever estimated from a weight or a size, a figure under four or over 300 is not read, and with none stated the step keeps no duration, as before. A fry or a sear multiplies its window by the number of turns only where the step states the pan's room ("in batches of 6"). A figure the recipe stated, a condition's own words, and the cook's own times (estimateStepTime's you tier) all outrank it. A recipe stored before this keeps its shape until it is read again. IMPORT-PIPELINE.md owns where in the pipeline this runs.

Three things an assumed range must never do, all consequences of the same fact — it is a guess, not a claim:

  • It never replaces the condition. type stays until-condition and sourceText stays the source's exact words; the range is minimumMs/maximumMs added beside them, and no screen may show the range in the treatment it gives a figure the recipe actually stated.
  • It never survives a real figure. estimateStepTime's you tier — the cook's own history — already outranks anything published, assumed or not; the moment a dish has been cooked once, the guess is gone from the schedule that matters.
  • It never happens where the recipe committed to a number, or to nothing at all: an active step with no condition (op_serve) gets no invented wait, the same "an absent duration is still better than a bad one" reasoning the rest of this file uses for every other unparsed field. The one exception is the import's range for hands-on work (knife and hand work the page never times), which is the model's judgement of what the step is done to and only a range (IMPORT-PIPELINE.md); a step that leaves nothing is still given none.

estimateStepTime answers "how long does this take" from the most specific source that has an answer, and always says which one it used:

  1. you — the median of your kitchen's past cooks of that step. "You" is the household, not the member: whose the evenings are is settled once, in Kitchens
  2. everyone — the median across all cooks (the function takes it and no caller supplies one, left absent rather than faked)
  3. recipe — what the author claimed

Median rather than mean, so one interrupted cook doesn't move the number much. A step is timed by the gap since the last step you finished, so timing costs the cook no taps; an explicit start/stop wins whenever one exists.

The you tier is read on the cooking screen only, behind Details — "You took 4 min last time", information rather than instruction. It does not plan the dish. The brief once re-planned the whole recipe from these medians the moment you cooked it once, so a single slow evening silently retimed every step; the ask was "way more chill", and what a cook's own history changes now is one opted-into total (below).

estimateStepTime itself reads no version at all — it trusts the id. gatherYourDurations keys purely on OperationNode.id, so whether a duration measured last month still answers for a step today rests entirely on whether that id still means the same job — see What an operation id promises. Today's only caller narrows the evidence it hands in to sessions of the same recipe version (useCookHistory's evidenceFrom, apps/web/src/hooks/cook-evidence.ts) as a belt-and-braces policy on top, not because the function needs it.

A range is not a number

squeezy may supply a range, never a point — "20 to 30 min" says what it is, an expectation with room in it, where "25 min" would claim a precision nobody has.

From COOKING.md, What assumed means