@@ -8,91 +8,172 @@ between prose and code, which works at every viewport.
88
99## Goals
1010
11+ - ** One column model per page type, fixed.** Example pages keep cells in
12+ the prose|code 2-col grid; journey pages keep section heading + figure
13+ in a 2-col grid. Figures never reflow the surrounding columns.
1114- ** Universal, not viewport-conditional.** A reader at any width sees the
12- same figure in the same place. No ` @media ` breakpoints; no overlay layer.
15+ same figure in the same place. No ` @media ` breakpoints for figure
16+ positioning; no overlay layer.
17+ - ** Multiple figures supported.** Banners hold one, two, or three
18+ figures via an auto-fit grid; small multiples are first-class.
1319- ** No contributor burden.** Example markdown stays as it is. Figures are
1420 curated separately by the project owner.
15- - ** Inline, between prose and code.** A figure sits in the same flow as the
16- cell prose and code; the cell stops being two columns and stacks
17- vertically so prose → figure → code reads top to bottom.
18- - ** Quiet by default.** Cells without figures keep the existing
19- ` prose | code ` grid unchanged. Today's pages render bit-for-bit identical
20- to before until a figure is attached.
21+ - ** Quiet by default.** A page with no figures attached renders
22+ bit-for-bit identical to today.
2123- ** Grammar reuse.** Figures are composed from the locked vocabulary in
2224 ` src/marginalia_grammar.py ` . No bespoke SVG.
2325
2426## Layout strategy
2527
26- Each ` .lp-cell ` is normally `grid-template-columns: minmax(17rem, .85fr)
27- minmax(0, 1fr)` — prose left, code right. When the cell has an attached
28- figure, the renderer adds the ` has-figure ` class:
28+ Two patterns, one for each page type. Both keep their underlying column
29+ model fixed; figures slot into defined positions without disrupting it.
2930
30- ``` css
31- .lp-cell.has-figure { grid-template-columns : 1fr ; }
32- ```
31+ ### Example pages — banners between cells
3332
34- Within that single column the children stack in document order: prose
35- paragraph, figure (with optional caption), code-stack. Cells without a
36- figure are not touched.
33+ Each ` .lp-cell ` stays `grid-template-columns: minmax(17rem, .85fr)
34+ minmax(0, 1fr)` — prose left, code right — ** always** . Figures live in
35+ banner rows that sit between cells, not inside them. The banner spans
36+ the full content width and uses an auto-fit grid so it can hold one,
37+ two, or three figures as small multiples.
3738
3839```
39- ┌── lp-cell (no figure, default) ──────────────────────┐
40- │ prose paragraph │ source / output │
41- └──────────────────────────────────────────────────────┘
42-
43- ┌── lp-cell.has-figure (single column) ───────────────┐
44- │ prose paragraph │
45- │ ┌──── cell-figure ────┐ │
46- │ │ svg │ │
47- │ └─────────────────────┘ │
48- │ caption (italic, muted) │
49- │ source │
50- │ output │
51- └──────────────────────────────────────────────────────┘
40+ [ cell 0 · prose | code ]
41+ ─────── banner row ───────
42+ [ figure ] optional caption
43+ ─────────────────────────
44+ [ cell 1 · prose | code ]
5245```
5346
54- The figure renders inside the cell, between ` <div class="lp-prose"> ` and
55- ` <div class="cell-code-stack"> ` :
47+ This matches the union of three influences:
48+ - * Knuth* — cells preserve the literate-program rhythm of prose and code
49+ side by side, uninterrupted.
50+ - * Tufte* — the banner slot accepts a small-multiple of related figures
51+ so contrasts and progressions read as one composition.
52+ - * Algebrica* — each banner figure carries a quiet italic caption beneath,
53+ in the muted text colour, with generous whitespace above and below.
54+
55+ Banner positions:
56+
57+ | key | renders |
58+ | --------------------| --------------------------------------|
59+ | ` before ` | once, before the first cell |
60+ | ` after-cell-N ` | once, after cell N (zero-indexed) |
61+ | ` after-walkthrough ` | once, after the last cell |
62+
63+ Each position holds ** one or more** figures via ` cell-banner ` markup.
64+ Captions are per-figure.
5665
5766``` html
58- <section class =" lesson-step lp-cell has-figure" >
59- <div class =" lp-prose" >…</div >
60- <figure class =" cell-figure" >
61- <svg >…</svg >
62- <figcaption >…</figcaption >
63- </figure >
64- <div class =" cell-code-stack" >…</div >
67+ <section class =" literate-program" >
68+ <section class =" lp-cell" >prose | code</section >
69+ <div class =" cell-banner cell-banner--2" >
70+ <figure >
71+ <svg >…</svg >
72+ <figcaption >Mutable: change visible through any alias.</figcaption >
73+ </figure >
74+ <figure >
75+ <svg >…</svg >
76+ <figcaption >Immutable: aliases share a frozen value.</figcaption >
77+ </figure >
78+ </div >
79+ <section class =" lp-cell" >prose | code</section >
6580</section >
6681```
6782
68- ` .cell-figure ` keeps the SVG ` width: 100%; max-width: 360px; ` so figures
69- display at a comfortable reading size on prose-column-wide cells without
70- ballooning when the column is wider.
83+ ``` css
84+ .cell-banner {
85+ margin : var (--space-5 ) 0 ;
86+ padding : var (--space-4 ) 0 ;
87+ border-block : 1px dashed var (--hairline-soft );
88+ display : grid ;
89+ grid-template-columns : repeat (auto-fit , minmax (220px , 1fr ));
90+ gap : var (--space-4 );
91+ justify-items : center ;
92+ }
93+ .cell-banner figure { margin : 0 ; padding : 0 ; max-width : 360px ; }
94+ .cell-banner svg { width : 100% ; height : auto ; display : block ; }
95+ .cell-banner figcaption {
96+ margin-top : var (--space-2 );
97+ color : var (--muted );
98+ font-size : .92rem ;
99+ font-style : italic ;
100+ max-width : 44ch ;
101+ }
102+ .cell-banner--1 figure { max-width : 440px ; }
103+ ```
104+
105+ The cell never reflows: cells without banners around them and cells
106+ between banners look identical to today's layout.
107+
108+ ### Journey pages — figure beside section heading
109+
110+ Journey pages are not literate code; they are linear lists of items
111+ grouped under section headings. Here the figure lives ** beside** the
112+ section heading in a 2-column row (heading-and-list on the left, figure
113+ on the right). The column model is fixed for the whole journey page:
114+ each section is a 2-col grid, every section the same shape.
115+
116+ ``` css
117+ .journey-section {
118+ display : grid ;
119+ grid-template-columns : minmax (0 , 1fr );
120+ gap : var (--space-4 );
121+ }
122+ @media (min-width : 900px ) {
123+ .journey-section {
124+ grid-template-columns : minmax (0 , 1.4fr ) minmax (220px , 320px );
125+ align-items : start ;
126+ }
127+ }
128+ ```
129+
130+ One figure per section, faithful to the section's conceptual shift.
131+ This is the journey-streams pattern; reused unchanged for all six
132+ journeys (see ` JOURNEY_SECTION_FIGURES ` in ` scripts/build_prototypes.py ` ).
133+
134+ ### Why these two, not five
135+
136+ The earlier prototypes that mixed inline-above, inline-between,
137+ after-output, and prose-aside all switched the cell's column model
138+ when a figure was present. That created the perils of randomly
139+ oscillating between 1, 2, and 3 columns within the same page. The
140+ banner-between grammar fixes the column model and lets figure count
141+ vary instead. Adding a second or third figure changes the banner's
142+ internal grid (auto-fit handles 1/2/3+), but the cells around it
143+ remain unchanged — no reflow, no cognitive context-switch.
71144
72145## Anchors and attachments
73146
74- ` src/marginalia.py ` declares which figures attach where:
147+ ` src/marginalia.py ` declares which figures attach where. The data shape
148+ will move from per-cell injection toward per-position banners:
75149
76150``` python
77- ATTACHMENTS = {
78- " mutability" : [
79- (" cell-0" , " aliasing-mutation" ,
80- " Two names share one mutable list — appending through one name "
81- " changes the object visible through both." ),
82- ],
83- # …
151+ # proposed shape — banners keyed by position, each holding 1+ figures
152+ BANNERS = {
153+ " mutability" : {
154+ " after-cell-0" : [
155+ (" aliasing-mutation" ,
156+ " Two names share one mutable list — appending through one "
157+ " name changes the object visible through both." ),
158+ (" tuple-no-mutation" ,
159+ " By contrast, a tuple is frozen — aliases share a value "
160+ " no method can change in place." ),
161+ ],
162+ },
84163}
85164```
86165
87- Anchor identifiers :
166+ Banner positions :
88167
89- | anchor | targets |
90- | -----------------| --------------------------------------|
91- | ` cell-0 ` , ` cell-1 ` , … | each literate-program cell, zero-indexed |
168+ | position | renders |
169+ | --------------------| --------------------------------------|
170+ | ` before ` | once, before the first cell |
171+ | ` after-cell-0 ` , … | once, after cell N (zero-indexed) |
172+ | ` after-walkthrough ` | once, after the last cell |
92173
93- Other anchors ( ` intro ` , ` notes ` , ` playground ` ) are reserved for future use
94- but only ` cell-N ` is wired today . Most slugs will start with no entry .
95- Adding a figure is a one-line edit in ` src/marginalia.py ` .
174+ Each position is a list, not a single figure: the same banner may hold
175+ multiple figures as a small multiple . Most slugs will start empty .
176+ Adding a banner is a one-line edit in ` src/marginalia.py ` .
96177
97178## Authoring model
98179
@@ -125,26 +206,34 @@ from grammar primitives) and register it in `FIGURES`. Append a tuple of
125206## Files
126207
127208- `src/marginalia_grammar.py` — palette, tokens, words, phrases, metrics.
128- Aligned with `public/site.css` design tokens; cards use the four palette
129- constants and never pick colours directly.
130- - `src/marginalia.py` — figure registry (`FIGURES`) and attachment map
131- (`ATTACHMENTS`). Exports `render_for_anchor(slug, anchor)` returning the
132- HTML the renderer injects into each cell.
133- - `src/app.py` — `_render_walkthrough_cell` adds the `has-figure` class
134- and inserts the figure between prose and code-stack when an attachment
135- exists.
136- - `public/site.css` — `.lp-cell.has-figure` and `.cell-figure` rules.
209+ Aligned with `public/site.css` design tokens; figures use the four
210+ palette constants and never pick colours directly.
211+ - `src/marginalia.py` — figure registry (`FIGURES`) and attachment map.
212+ Exports `render_for_anchor(slug, anchor)` for the current cell-inline
213+ layout; banner-rendering helpers will land alongside.
214+ - `src/app.py` — `_render_walkthrough_cell` is the current rendering
215+ helper; the banner-between rollout will rename or replace it with a
216+ walkthrough-level renderer that interleaves cells and banners.
217+ - `public/site.css` — currently `.lp-cell.has-figure` and `.cell-figure`
218+ rules; will gain `.cell-banner` rules when the banner grammar ships
219+ in production.
220+ - `scripts/build_prototypes.py` — already implements the banner grammar
221+ and journey-section grammar so prototypes can validate it before
222+ production migration.
137223
138224## Edge cases
139225
140- - **Two figures on one cell.** Allowed; both render in document order
141- inside the `has-figure` cell. Use sparingly — three or more is a signal
142- the cell's prose is doing too much.
143- - **No figure attached.** The cell keeps today's `prose | code` 2-column
144- grid; no DOM, CSS, or layout difference from before.
145- - **Print.** The single-column stacking is print-friendly by default.
146- - **Very narrow viewports (≤340px).** SVGs scale via `max-width: 100%`.
147- The grammar's intrinsic text sizes stay readable down to ~280px.
226+ - **Many figures in one banner.** Auto-fit grid handles 1 (centered,
227+ larger), 2 (small-multiple pair), 3+ (wraps as content allows). More
228+ than 3 in one banner is usually a signal that two adjacent banners
229+ are clearer.
230+ - **No figures attached.** The page renders bit-for-bit identical to
231+ today.
232+ - **Print.** Banners and cells both flow naturally in single-column
233+ print contexts.
234+ - **Very narrow viewports (≤340px).** Banner figures stack via the
235+ auto-fit grid; SVGs scale via `max-width: 100%`. Cells keep their
236+ existing 980px breakpoint for collapsing the 2-col grid.
148237
149238## Non-goals
150239
0 commit comments