What changed, and why

The collection's closings had drifted. They lived in different places (sections/ vs components/), went by different names (Coda, Closing, Onward, Synthesis, Family), and a few were buried inline at the tail of a content chapter or missing a beat outright. A read-only audit — one agent per lesson — graded all 23 against a single coda contract: a distinct close that names the one idea and points where to go next, deduped, in the lesson's own voice.

Twelve already passed and are untouched. Eleven are corrected here, in three buckets: seven gained the missing where to go next beat; four had their one idea sharpened; two had competing closings deduped. Every fix reuses the lesson's existing, already-dual-mode classes — or, where a new class was unavoidable, ships it with both light and dark. The sections below read one representative change per bucket, plus the two structural ones and a real bug the isolation scan caught.

The common fix — add the where-next beat

Eight of the eleven simply lacked a forward pointer: a crisp one-idea, then the lesson stopped (or jumped straight to a bibliography). The fix is additive and themes for free, because it reuses the lesson's own eyebrow and prose classes. SWIM is representative — three real onward pointers (Lifeguard, Raft/Paxos above it, anti-entropy gossip) in its measured, intimate voice.

A where-next block built from existing swim-eyebrow / swim-prose classes.

src/lessons/swim/components/Closing.jsx · 119 lines
src/lessons/swim/components/Closing.jsx119 lines · JSX
⋯ 43 lines hidden (lines 1–43)
1export function Closing() {
2 return (
3 <section className="swim-section" style={{ paddingBottom: 140 }}>
4 <div className="swim-page">
5 <div className="swim-narrow" style={{ margin: '0 auto' }}>
6 <div className="swim-eyebrow" style={{ marginBottom: 28, color: 'var(--brass)' }}>
7 Coda
8 </div>
9 <h2
10 className="swim-display"
11 style={{
12 fontSize: 'clamp(36px, 5vw, 56px)',
13 margin: 0,
14 color: 'var(--ink-bright)',
15 lineHeight: 1.05,
16 }}
17 >
18 What you are <em>looking at</em>
19 </h2>
20 <div className="swim-rule-short" style={{ margin: '28px 0' }} />
21 <div className="swim-prose" style={{ fontSize: 17, lineHeight: 1.75 }}>
22 <p>
23 A protocol that takes a structurally simple question, <em>who is here?</em>, and
24 answers it across thousands of machines without ever asking everyone at once. The
25 cleverness is not in any one mechanism but in the way five small ideas compose. Count
26 them. The indirect probe kills false positives at constant cost. The suspicion state
27 breaks the tradeoff between detection speed and commitment. The incarnation number
28 resolves disagreement with the minimum information needed. The piggyback rides every
29 packet that was going out anyway. The epidemic spread reaches a thousand nodes in
30 about ten rounds.
31 </p>
32 <p>
33 Each piece by itself is unsurprising. Each piece is doing something the others could
34 not. Together they describe a particular way of being correct in a network. It refuses
35 to pay for properties it does not need, and uses the savings to scale.
36 </p>
37 <p>
38 The protocol does not provide consensus, ordering, or moment-to-moment agreement. It
39 provides something less, on purpose.{' '}
40 <em>The cluster knows, eventually, who is here.</em> That much is enough to build the
41 rest on.
42 </p>
43 </div>
44 <div style={{ marginTop: 56 }}>
45 <div className="swim-eyebrow" style={{ marginBottom: 24, color: 'var(--brass)' }}>
46 Where to go next
47 </div>
48 <div className="swim-prose swim-mid" style={{ fontSize: 17, lineHeight: 1.75 }}>
49 <p>
50 <strong style={{ color: 'var(--brass)' }}>Lifeguard</strong>, if you intend to run
51 this. The three refinements from §09 a node that <em>handicaps itself</em> when
52 its own health degrades, suspicion timeouts that decay as witnesses pile on, and a
53 word sent straight to the accused are what turn the paper's clean object into
54 something that survives slow nodes and GC pauses. SWIM with Lifeguard is what ships
55 inside Serf, Consul, and Nomad.
56 </p>
57 <p>
58 <strong style={{ color: 'var(--brass)' }}>Raft, or Paxos</strong>, for the rung
59 above. Membership is not consensus, and SWIM was careful never to promise it. When
60 you need an ordered, linearizable decision about shared state, SWIM tells you who
61 the members <em>are</em> and a consensus log decides what they <em>agree to</em> —
62 the cheap eventual layer at the bottom, the expensive unanimous-now layer on top. It
⋯ 57 lines hidden (lines 63–119)
63 is the same two-rung ladder the comparison in §10 ends on, and Consul is its
64 canonical shape.
65 </p>
66 <p>
67 <strong style={{ color: 'var(--brass)' }}>Anti-entropy gossip</strong>, for moving
68 more than just <em>who is here</em>. SWIM piggybacks membership deltas; its cousin
69 gossips arbitrary key-value state, with Merkle trees making the reconciliation diff
70 cheap. It is how Dynamo, Cassandra, and Riak keep replicas converging — the same
71 epidemic spread, pointed at data instead of liveness.
72 </p>
73 </div>
74 </div>
75 <div style={{ marginTop: 64, display: 'flex', alignItems: 'center', gap: 16 }}>
76 <span
77 style={{
78 display: 'inline-block',
79 width: 6,
80 height: 6,
81 background: 'var(--brass)',
82 transform: 'rotate(45deg)',
83 }}
84 />
85 <span
86 className="swim-mono"
87 style={{ fontSize: 11, color: 'var(--ink-faint)', letterSpacing: '0.16em' }}
88 >
89 END
90 </span>
91 <div
92 style={{
93 flex: 1,
94 height: 1,
95 background: 'linear-gradient(90deg, var(--brass-deep), transparent)',
96 }}
97 />
98 </div>
99 <div
100 style={{
101 marginTop: 28,
102 fontFamily: 'JetBrains Mono',
103 fontSize: 10.5,
104 color: 'var(--ink-label)',
105 lineHeight: 1.7,
106 }}
107 >
108 <div>Das, A., Gupta, I., & Motivala, A. (2002).</div>
109 <div>SWIM: Scalable Weakly-consistent Infection-style Process Group Membership.</div>
110 <div>International Conference on Dependable Systems and Networks.</div>
111 <div style={{ marginTop: 10 }}>Dadgar, A., Phillips, J., & Currey, J. (2018).</div>
112 <div>Lifeguard: Local Health Awareness for More Accurate Failure Detection.</div>
113 <div>HashiCorp Research.</div>
114 </div>
115 </div>
116 </div>
117 </section>
118 );

Sharpening the one idea

Four codas had a weaker problem: they ended on several takeaways instead of one. Concurrency-foundations is the clearest case — it closed on a three-card grid that read as three competing conclusions. That grid is replaced by a single one-idea box, with the three directions demoted to a proper where to go next list below.

One synthesis box (.cc-one-idea), then a where-next list reusing .closing-card.

src/lessons/concurrency-foundations/components/Coda.jsx · 106 lines
src/lessons/concurrency-foundations/components/Coda.jsx106 lines · JSX
⋯ 64 lines hidden (lines 1–64)
1import React from 'react';
2 
3const NEXT = [
4 {
5 key: 'memory-models',
6 num: 'I',
7 title: 'Hardware memory models',
8 body: (
9 <React.Fragment>
10 Drop below <em>happens-before</em> to where it is enforced: x86&apos;s strong TSO, ARM and
11 RISC-V&apos;s weak ordering, and the fences a compiler emits to bridge them. Same source,
12 very different arrows.
13 </React.Fragment>
14 ),
15 },
16 {
17 key: 'lock-free',
18 num: 'II',
19 title: 'Lock-free data structures',
20 body: (
21 <React.Fragment>
22 Treiber stacks, Michael&ndash;Scott queues, and the ABA problem. What it costs to let many
23 threads observe and mutate a structure with no lock holding the line.
24 </React.Fragment>
25 ),
26 },
27 {
28 key: 'consensus',
29 num: 'III',
30 title: 'Distributed consensus',
31 body: (
32 <React.Fragment>
33 The same question across machines instead of threads: which observations of a shared value
34 are allowed when the wire can drop, delay, and reorder. Paxos and Raft are the canonical
35 answers.
36 </React.Fragment>
37 ),
38 },
39 {
40 key: 'formal',
41 num: 'IV',
42 title: 'Formal verification',
43 body: (
44 <React.Fragment>
45 TLA+ and model checking let you state &ldquo;which interleavings are allowed&rdquo; as an
46 invariant and have a machine search for the one that breaks it &mdash; the bug you would
47 otherwise meet in production.
48 </React.Fragment>
49 ),
50 },
51];
52 
53export function Coda() {
54 return (
55 <section className="section section-coda">
56 <div className="section-num">coda</div>
57 <h2 className="section-title">
58 What you can now <em>see</em>.
59 </h2>
60 <p className="prose">
61 Concurrency is hard for one reason. Programs that share state can be observed in
62 inconsistent ways. Every primitive in this lesson is a way of saying which observations are
63 allowed, which are forbidden, and who gets to decide. Mutexes, atomics, channels, async
64 tasks, actors, memory orderings: each one draws that line somewhere.
65 </p>
66 
67 <div className="cc-one-idea">
68 <div className="cc-one-idea-label">The one idea</div>
69 <p className="cc-one-idea-body">
70 Every synchronization primitive &mdash; mutex, atomic, channel, actor &mdash; is one
71 answer to a single question:{' '}
72 <em>
73 which observations across threads are allowed, which are forbidden, and who decides.
74 </em>{' '}
75 Underneath them all sit <em>happens-before</em> edges; the arrows are where visibility
76 stops being something you hope for and becomes something you can reason about. The shape
77 of those answers <em>is</em> whether your program is correct.
78 </p>
79 </div>
80 
81 <p className="prose">
82 Most production code mercifully never descends into the depths you&apos;ve now visited. But
83 the depths are real. When an &ldquo;impossible&rdquo; bug surfaces, you&apos;ll recognize
84 the terrain immediately, and you&apos;ll know which floor of the building it lives on.
85 </p>
86 
87 <p className="section-num" style={{ marginTop: '2.5rem' }}>
88 where to go next
89 </p>
90 <div className="closing-grid">
91 {NEXT.map((it) => (
92 <div className="closing-card" key={it.key}>
93 <div className="closing-num">{it.num}</div>
94 <div className="closing-card-title">{it.title}</div>
95 <div className="closing-card-body">{it.body}</div>
96 </div>
97 ))}
98 </div>
99 
⋯ 7 lines hidden (lines 100–106)
100 <div className="pull-quote" style={{ borderLeftColor: 'var(--cyan)', maxWidth: '38rem' }}>
101 Modern hardware is fast because it doesn&apos;t synchronize. Modern software is correct
102 because it knows where, exactly, to ask it to.
103 </div>
104 </section>
105 );

A new class, themed both ways

Where a one-idea box did not already exist, a new class was added — but never in one mode only. gRPC's .gx-kicker rides existing tokens (it mirrors .gx-eyebrow), and .gx-coda ships an explicit [data-theme='light'] complement alongside its dark base, exactly how the lesson themes its other panels.

Base rules near the top; the light complement lives with the lesson's other [data-theme] overrides.

src/lessons/grpc/grpc.css · 906 lines
src/lessons/grpc/grpc.css906 lines · CSS
⋯ 169 lines hidden (lines 1–169)
1@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300..800&family=Instrument+Sans:ital,wght@0,400..600;1,400..500&display=swap');
2 
3/* gRPC — "on the wire". A protocol-analyzer / signal-scope instrument deck.
4 Per-lesson identity: own dark paper (#070b0f), cool signal ink, a faint
5 graticule, and one byte colour code throughout —
6 amber = field tag violet = length prefix lime = payload value
7 cyan = live signal coral = a protocol sin mint = compatible/verified
8 Family glue is kept: the mono is the shared JetBrains Mono (var(--font-mono)),
9 never a per-lesson face. Display is Bricolage Grotesque, body Instrument Sans. */
10 
11.gx {
12 --bg: #070b0f;
13 --bg2: #0a1015;
14 --panel: #0d141b;
15 --panel2: #111a23;
16 --panel3: #16222d;
17 --line: #1a2731;
18 --line2: #273844;
19 --line3: #34495a;
20 
21 --ink: #cfdde5;
22 --ink-bright: #f1f8fb;
23 --ink-dim: #8398a4;
24 --ink-faint: #56697450;
25 --ink-faint2: #566974;
26 /* functional micro-labels share faint2's hue but lifted to WCAG AA on the
27 darkest grounds (panel2/well/bg); decorative faint2 uses stay un-lifted */
28 --ink-faint2-fn: #738692;
29 
30 --cyan: #38ddcb;
31 --cyan-deep: #0c6b63;
32 --cyan-glow: rgba(56, 221, 203, 0.16);
33 --amber: #f4b13c;
34 --amber-glow: rgba(244, 177, 60, 0.15);
35 --violet: #a89bff;
36 --violet-glow: rgba(168, 155, 255, 0.14);
37 --lime: #9fd35f;
38 --lime-glow: rgba(159, 211, 95, 0.13);
39 --coral: #ff6b6b;
40 --coral-glow: rgba(255, 107, 107, 0.14);
41 --mint: #4fd99b;
42 --mint-glow: rgba(79, 217, 155, 0.14);
43 
44 /* surfaces/ink painted inline in JSX (mirror the CSS code/prose flips) */
45 --gx-well: #060a0e; /* near-black data well (code/byte/schema surfaces) */
46 --gx-prose-ink: #b9cad3; /* prose body ink painted inline */
47 
48 font-family: 'Instrument Sans', system-ui, sans-serif;
49 font-weight: 400;
50 color: var(--ink);
51 background: var(--bg);
52 line-height: 1.62;
53 -webkit-font-smoothing: antialiased;
54 text-rendering: optimizeLegibility;
55 position: relative;
56 overflow-x: hidden;
57 min-height: 100vh;
59.gx *,
60.gx *::before,
61.gx *::after {
62 box-sizing: border-box;
64 
65/* graticule + atmosphere */
66.gx::before {
67 content: '';
68 position: fixed;
69 inset: 0;
70 z-index: 0;
71 pointer-events: none;
72 background-image:
73 linear-gradient(var(--line) 1px, transparent 1px),
74 linear-gradient(90deg, var(--line) 1px, transparent 1px);
75 background-size: 46px 46px;
76 -webkit-mask-image: radial-gradient(ellipse 120% 90% at 50% 0%, #000 0%, transparent 75%);
77 mask-image: radial-gradient(ellipse 120% 90% at 50% 0%, #000 0%, transparent 75%);
78 opacity: 0.55;
80.gx::after {
81 content: '';
82 position: fixed;
83 inset: 0;
84 z-index: 0;
85 pointer-events: none;
86 background-image:
87 radial-gradient(ellipse 70% 50% at 50% -5%, var(--cyan-glow), transparent 60%),
88 radial-gradient(ellipse 50% 40% at 92% 12%, var(--violet-glow), transparent 60%),
89 radial-gradient(ellipse 60% 45% at 6% 88%, var(--amber-glow), transparent 60%);
90 opacity: 0.7;
92.gx-noise {
93 position: fixed;
94 inset: 0;
95 z-index: 0;
96 pointer-events: none;
97 opacity: 0.025;
98 mix-blend-mode: screen;
99 background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
101.gx-wrap {
102 position: relative;
103 z-index: 1;
105 
106/* ---- layout ---- */
107.gx-section {
108 max-width: 920px;
109 margin: 0 auto;
110 padding: 0 24px;
112.gx-section.narrow {
113 max-width: 760px;
115.gx-block {
116 padding: 60px 0;
118 
119@media (max-width: 640px) {
120 .gx {
121 line-height: 1.58;
122 }
123 .gx-section {
124 padding: 0 17px;
125 }
126 .gx-block {
127 padding: 42px 0;
128 }
129 .gx::before {
130 background-size: 34px 34px;
131 }
133 
134/* ---- typography ---- */
135.gx h1,
136.gx h2,
137.gx h3 {
138 font-family: 'Bricolage Grotesque', sans-serif;
139 font-weight: 600;
140 line-height: 1.04;
141 letter-spacing: -0.02em;
142 color: var(--ink-bright);
143 margin: 0;
145.gx-eyebrow {
146 font-family: var(--font-mono);
147 font-size: 11px;
148 font-weight: 500;
149 letter-spacing: 0.26em;
150 text-transform: uppercase;
151 color: var(--cyan);
152 display: inline-flex;
153 align-items: center;
154 gap: 9px;
156.gx-eyebrow::before {
157 content: '';
158 width: 22px;
159 height: 1px;
160 background: var(--cyan);
161 opacity: 0.6;
163.gx-h2 {
164 font-size: clamp(28px, 5.4vw, 46px);
165 margin: 16px 0 0;
167 
168/* coda one-idea: a framed synthesis block (mirrors .gx-eyebrow's mono/cyan
169 kicker, and the .gx-panel surface idiom). Tokens flip for light/dark. */
170.gx-kicker {
171 font-family: var(--font-mono);
172 font-size: 11px;
173 font-weight: 500;
174 letter-spacing: 0.26em;
175 text-transform: uppercase;
176 color: var(--cyan);
177 margin: 0;
179.gx-coda {
180 background: linear-gradient(180deg, var(--cyan-glow), transparent);
181 border: 1px solid var(--line2);
182 border-left: 2px solid var(--cyan);
183 border-radius: 12px;
184 padding: 18px 20px;
185 margin: 16px 0 0;
187.gx-h3 {
188 font-size: clamp(20px, 3.2vw, 26px);
189 margin: 0 0 10px;
190 font-weight: 600;
192.gx-lede {
193 font-size: clamp(16px, 2.3vw, 19px);
194 color: var(--ink-dim);
195 margin: 16px 0 0;
196 max-width: 60ch;
⋯ 697 lines hidden (lines 197–893)
198.gx p {
199 margin: 0 0 17px;
201.gx p.tight {
202 margin-bottom: 10px;
204.gx-prose {
205 font-size: 16.5px;
207.gx-prose p {
208 color: #b9cad3;
210.gx-prose strong {
211 color: var(--ink-bright);
212 font-weight: 600;
214.gx-prose em {
215 color: var(--cyan);
216 font-style: normal;
218.gx-prose a {
219 color: var(--cyan);
220 text-decoration: none;
221 border-bottom: 1px solid var(--cyan-deep);
223@media (max-width: 640px) {
224 .gx-prose {
225 font-size: 15.5px;
226 }
228 
229.gx-kw {
230 font-family: var(--font-mono);
231 font-size: 0.86em;
232 color: var(--amber);
233 background: var(--amber-glow);
234 padding: 1px 6px;
235 border-radius: 4px;
236 letter-spacing: -0.01em;
238.gx-term {
239 color: var(--ink-bright);
240 font-weight: 600;
241 border-bottom: 1px dotted var(--line3);
242 cursor: help;
244 
245/* dropcap */
246.gx-dropcap::first-letter {
247 font-family: 'Bricolage Grotesque', sans-serif;
248 font-weight: 700;
249 font-size: 3.5em;
250 line-height: 0.78;
251 float: left;
252 margin: 6px 12px 0 0;
253 color: var(--cyan);
254 -webkit-text-fill-color: var(--cyan);
256 
257/* ---- panel / card ---- */
258.gx-panel {
259 background: linear-gradient(180deg, var(--panel) 0%, var(--bg2) 100%);
260 border: 1px solid var(--line);
261 border-radius: 14px;
262 position: relative;
263 overflow: hidden;
265.gx-panel.pad {
266 padding: 22px;
268@media (max-width: 640px) {
269 .gx-panel.pad {
270 padding: 16px;
271 }
273.gx-panel-label {
274 font-family: var(--font-mono);
275 font-size: 10px;
276 font-weight: 500;
277 letter-spacing: 0.2em;
278 text-transform: uppercase;
279 color: var(--ink-faint2);
280 display: flex;
281 align-items: center;
282 gap: 8px;
283 margin-bottom: 14px;
285.gx-panel-label .dot {
286 width: 6px;
287 height: 6px;
288 border-radius: 50%;
289 background: var(--cyan);
290 box-shadow: 0 0 8px var(--cyan);
292 
293/* ---- code ---- */
294.gx-code {
295 font-family: var(--font-mono);
296 font-size: 12.5px;
297 line-height: 1.7;
298 background: #060a0e;
299 border: 1px solid var(--line);
300 border-radius: 11px;
301 padding: 16px 17px;
302 overflow-x: auto;
303 color: #b9cdd6;
304 letter-spacing: -0.02em;
305 position: relative;
306 white-space: pre-wrap;
307 word-break: normal;
309.gx-code .cm {
310 color: var(--ink-faint2);
311} /* comment */
312.gx-code .kw {
313 color: var(--violet);
314} /* keyword */
315.gx-code .ty {
316 color: var(--cyan);
317} /* type */
318.gx-code .fn {
319 color: var(--amber);
320} /* function/field */
321.gx-code .nu {
322 color: var(--lime);
323} /* number */
324.gx-code .st {
325 color: var(--lime);
326} /* string */
327.gx-code .pu {
328 color: var(--ink-dim);
329} /* punctuation dim */
330@media (max-width: 640px) {
331 .gx-code {
332 font-size: 11.5px;
333 padding: 13px 13px;
334 }
336 
337/* ---- controls ---- */
338.gx-btn {
339 font-family: var(--font-mono);
340 font-size: 11px;
341 font-weight: 500;
342 letter-spacing: 0.06em;
343 text-transform: uppercase;
344 background: var(--panel2);
345 color: var(--ink);
346 border: 1px solid var(--line2);
347 border-radius: 8px;
348 padding: 9px 14px;
349 cursor: pointer;
350 transition: all 0.16s ease;
351 -webkit-tap-highlight-color: transparent;
352 user-select: none;
354.gx-btn:hover {
355 border-color: var(--line3);
356 color: var(--ink-bright);
358.gx-btn.on {
359 background: var(--cyan-glow);
360 border-color: var(--cyan);
361 color: var(--cyan);
363.gx-seg {
364 display: inline-flex;
365 flex-wrap: wrap;
366 gap: 6px;
368.gx-input {
369 font-family: var(--font-mono);
370 font-size: 13px;
371 color: var(--ink-bright);
372 background: #060a0e;
373 border: 1px solid var(--line2);
374 border-radius: 8px;
375 padding: 8px 11px;
376 width: 100%;
377 outline: none;
378 transition: border-color 0.16s;
380.gx-input:focus {
381 border-color: var(--cyan);
383.gx-range {
384 -webkit-appearance: none;
385 appearance: none;
386 width: 100%;
387 height: 4px;
388 border-radius: 2px;
389 background: var(--line2);
390 outline: none;
392.gx-range::-webkit-slider-thumb {
393 -webkit-appearance: none;
394 width: 18px;
395 height: 18px;
396 border-radius: 50%;
397 background: var(--cyan);
398 cursor: pointer;
399 box-shadow: 0 0 10px var(--cyan-glow);
400 border: 2px solid var(--bg);
402.gx-range::-moz-range-thumb {
403 width: 18px;
404 height: 18px;
405 border-radius: 50%;
406 background: var(--cyan);
407 cursor: pointer;
408 border: 2px solid var(--bg);
410 
411/* ---- byte tokens (the heart of the visual language) ---- */
412.gx-bytes {
413 display: flex;
414 flex-wrap: wrap;
415 gap: 5px;
416 font-family: var(--font-mono);
418.gx-byte {
419 font-size: 12px;
420 font-weight: 500;
421 padding: 5px 7px;
422 border-radius: 6px;
423 border: 1px solid var(--line2);
424 background: var(--panel2);
425 color: var(--ink);
426 letter-spacing: 0.02em;
427 transition: all 0.2s ease;
428 position: relative;
430.gx-byte.tag {
431 color: var(--amber);
432 border-color: var(--amber);
433 background: var(--amber-glow);
435.gx-byte.len {
436 color: var(--violet);
437 border-color: var(--violet);
438 background: var(--violet-glow);
440.gx-byte.val {
441 color: var(--lime);
442 border-color: var(--lime);
443 background: var(--lime-glow);
445.gx-byte.cont {
446 opacity: 0.95;
448@media (max-width: 640px) {
449 .gx-byte {
450 font-size: 11px;
451 padding: 4px 6px;
452 }
454 
455.gx-legend {
456 display: flex;
457 flex-wrap: wrap;
458 gap: 14px;
459 font-family: var(--font-mono);
460 font-size: 10.5px;
461 letter-spacing: 0.03em;
462 color: var(--ink-dim);
464.gx-legend span {
465 display: inline-flex;
466 align-items: center;
467 gap: 6px;
469.gx-legend i {
470 width: 11px;
471 height: 11px;
472 border-radius: 3px;
473 display: inline-block;
475 
476/* ---- callout ---- */
477.gx-callout {
478 border-radius: 12px;
479 padding: 16px 18px;
480 margin: 22px 0;
481 border: 1px solid;
482 display: flex;
483 gap: 13px;
484 align-items: flex-start;
485 font-size: 15px;
487.gx-callout .ic {
488 flex-shrink: 0;
489 margin-top: 1px;
491.gx-callout.warn {
492 background: var(--coral-glow);
493 border-color: #5e2a2a;
494 color: #ffd0d0;
496.gx-callout.warn b {
497 color: var(--coral);
499.gx-callout.ok {
500 background: var(--mint-glow);
501 border-color: #1f5240;
502 color: #c6f0dd;
504.gx-callout.ok b {
505 color: var(--mint);
507.gx-callout.info {
508 background: var(--cyan-glow);
509 border-color: var(--cyan-deep);
510 color: #c4ecea;
512.gx-callout.info b {
513 color: var(--cyan);
515 
516/* ---- misc ---- */
517.gx-rule {
518 height: 1px;
519 background: linear-gradient(90deg, transparent, var(--line2) 30%, var(--line2) 70%, transparent);
520 margin: 0 auto;
521 max-width: 920px;
523.gx-stat-row {
524 display: flex;
525 gap: 10px;
526 flex-wrap: wrap;
527 align-items: stretch;
529.gx-stat {
530 flex: 1;
531 min-width: 120px;
532 background: var(--panel2);
533 border: 1px solid var(--line);
534 border-radius: 10px;
535 padding: 13px 15px;
537.gx-stat .v {
538 font-family: 'Bricolage Grotesque';
539 font-size: 26px;
540 font-weight: 700;
541 color: var(--ink-bright);
542 line-height: 1;
544.gx-stat .v.cy {
545 color: var(--cyan);
547.gx-stat .v.am {
548 color: var(--amber);
550.gx-stat .k {
551 font-family: var(--font-mono);
552 font-size: 9.5px;
553 letter-spacing: 0.16em;
554 text-transform: uppercase;
555 color: var(--ink-faint2-fn);
556 margin-top: 6px;
558 
559.gx-fade {
560 opacity: 0;
561 transform: translateY(16px);
562 transition:
563 opacity 0.7s ease,
564 transform 0.7s ease;
566.gx-fade.in {
567 opacity: 1;
568 transform: none;
570@media (prefers-reduced-motion: reduce) {
571 .gx-fade {
572 opacity: 1 !important;
573 transform: none !important;
574 transition: none;
575 }
577 
578/* ---- hero wire (the request/response animation) ---- */
579.gx .hw-link {
580 position: relative;
581 flex: 1;
582 height: 64px;
583 min-width: 90px;
585.gx .hw-track {
586 position: absolute;
587 top: 50%;
588 left: 0;
589 right: 0;
590 height: 2px;
591 transform: translateY(-50%);
592 background: linear-gradient(90deg, var(--cyan-deep), var(--line3), var(--amber));
593 opacity: 0.5;
595.gx .hw-frame {
596 position: absolute;
597 top: 50%;
598 font-family: var(--font-mono);
599 font-size: 8px;
600 letter-spacing: 0.1em;
601 padding: 3px 6px;
602 border-radius: 4px;
603 white-space: nowrap;
605.gx .hw-frame.req {
606 background: var(--cyan-glow);
607 color: var(--cyan);
608 border: 1px solid var(--cyan);
609 transform: translateY(-50%);
610 animation: gxHwReq 3.4s cubic-bezier(0.6, 0, 0.4, 1) infinite;
612.gx .hw-frame.res {
613 background: var(--amber-glow);
614 color: var(--amber);
615 border: 1px solid var(--amber);
616 transform: translateY(-50%);
617 animation: gxHwRes 3.4s cubic-bezier(0.6, 0, 0.4, 1) infinite;
618 opacity: 0;
620@keyframes gxHwReq {
621 0% {
622 left: 0;
623 opacity: 0;
624 }
625 8% {
626 opacity: 1;
627 }
628 42% {
629 left: calc(100% - 56px);
630 opacity: 1;
631 }
632 50% {
633 opacity: 0;
634 }
635 100% {
636 left: calc(100% - 56px);
637 opacity: 0;
638 }
640@keyframes gxHwRes {
641 0%,
642 50% {
643 opacity: 0;
644 }
645 52% {
646 left: calc(100% - 60px);
647 opacity: 1;
648 }
649 92% {
650 left: 0;
651 opacity: 1;
652 }
653 100% {
654 left: 0;
655 opacity: 0;
656 }
658@media (prefers-reduced-motion: reduce) {
659 .gx .hw-frame.req,
660 .gx .hw-frame.res {
661 animation: none;
662 }
663 .gx .hw-frame.req {
664 left: 30%;
665 opacity: 1;
666 }
667 .gx .hw-frame.res {
668 opacity: 0;
669 }
671 
672/* ---- schema-evolution grid ---- */
673.gx .evo-grid {
674 display: grid;
675 grid-template-columns: 1fr auto 1fr;
676 align-items: center;
677 gap: 12px;
678 margin-bottom: 18px;
680.gx .evo-arrow {
681 display: grid;
682 place-items: center;
684@media (max-width: 560px) {
685 .gx .evo-grid {
686 grid-template-columns: 1fr;
687 }
688 .gx .evo-arrow {
689 transform: rotate(90deg);
690 }
692 
693/* ---- rpc call-shape stage ---- */
694.gx .rpc-stage {
695 display: grid;
696 grid-template-columns: 64px 1fr 64px;
697 gap: 10px;
698 align-items: stretch;
699 min-height: 200px;
701.gx .rpc-life {
702 display: flex;
703 flex-direction: column;
704 align-items: center;
706.gx .rpc-life span {
707 font-family: var(--font-mono);
708 font-size: 9.5px;
709 letter-spacing: 0.12em;
710 color: var(--ink-dim);
711 margin-bottom: 8px;
713.gx .rpc-bar {
714 flex: 1;
715 width: 2px;
716 border-radius: 2px;
718.gx .rpc-bar.cy {
719 background: linear-gradient(var(--cyan), var(--cyan-deep));
721.gx .rpc-bar.am {
722 background: linear-gradient(var(--amber), #6b4a13);
724.gx .rpc-mid {
725 display: flex;
726 flex-direction: column;
727 justify-content: center;
728 gap: 10px;
729 padding: 4px 0;
731.gx .rpc-msg {
732 display: flex;
733 align-items: center;
734 gap: 8px;
735 opacity: 0;
736 animation: gxRpcIn 0.5s ease forwards;
738.gx .rpc-msg.req {
739 justify-content: flex-start;
741.gx .rpc-msg.res {
742 justify-content: flex-end;
743 flex-direction: row-reverse;
744 --fromx: 12px;
746.gx .rpc-arrow {
747 font-family: var(--font-mono);
748 font-size: 18px;
750.gx .rpc-msg.req .rpc-arrow {
751 color: var(--cyan);
753.gx .rpc-msg.res .rpc-arrow {
754 color: var(--amber);
756.gx .rpc-tag {
757 font-family: var(--font-mono);
758 font-size: 11px;
759 padding: 4px 9px;
760 border-radius: 6px;
762.gx .rpc-msg.req .rpc-tag {
763 background: var(--cyan-glow);
764 color: var(--cyan);
765 border: 1px solid var(--cyan-deep);
767.gx .rpc-msg.res .rpc-tag {
768 background: var(--amber-glow);
769 color: var(--amber);
770 border: 1px solid #6b4a13;
772@keyframes gxRpcIn {
773 from {
774 opacity: 0;
775 transform: translateX(var(--fromx, -12px));
776 }
777 to {
778 opacity: 1;
779 transform: none;
780 }
782@media (prefers-reduced-motion: reduce) {
783 .gx .rpc-msg {
784 animation: none;
785 opacity: 1;
786 }
788 
789/* ---- three pillars grid ---- */
790.gx .pil-grid {
791 display: grid;
792 grid-template-columns: repeat(3, 1fr);
793 gap: 12px;
794 margin-top: 24px;
796@media (max-width: 640px) {
797 .gx .pil-grid {
798 grid-template-columns: 1fr;
799 }
801 
802/* ─── Light theme — a signal scope in daylight ───
803 The same instrument deck, now under bench light: a cool pale grey-cyan
804 panel of calibrated glass, ink readouts instead of glowing traces. Where
805 the dark mode floats neon over black, daylight prints the graticule onto
806 the panel and lets each signal settle into a deep, saturated ink. The
807 signature signal cyan is deepened so it reads as confidently on the bright
808 panel as it glowed on black; the byte colour-code keeps every meaning
809 (amber tag / violet length / lime payload / coral sin / mint verified).
810 Every body, label, and value pair was solved in OKLCH for WCAG AA on the
811 ground it actually sits on. */
812[data-theme='light'] .gx {
813 --bg: #dce5e6;
814 --bg2: #d2dde0;
815 --panel: #e6eded;
816 --panel2: #dae4e5;
817 --panel3: #cdd9db;
818 --line: #b9c6c8;
819 --line2: #9fb0b3;
820 --line3: #7e9094;
821 
822 --ink: #1f292d;
823 --ink-bright: #0d1417;
824 --ink-dim: #475459;
825 --ink-faint: #4d606740;
826 --ink-faint2: #4d6067;
827 /* in daylight the base faint2 already clears AA on its grounds — the
828 functional label needs no lift here, so it tracks faint2 */
829 --ink-faint2-fn: #4d6067;
830 
831 --cyan: #04574f;
832 --cyan-deep: #5fb3a8;
833 --cyan-glow: rgba(4, 87, 79, 0.12);
834 --amber: #7a4904;
835 --amber-glow: rgba(122, 73, 4, 0.11);
836 --violet: #4a3a8e;
837 --violet-glow: rgba(74, 58, 142, 0.1);
838 --lime: #3d5a0e;
839 --lime-glow: rgba(61, 90, 14, 0.12);
840 --coral: #a32424;
841 --coral-glow: rgba(163, 36, 36, 0.1);
842 --mint: #0b603d;
843 --mint-glow: rgba(11, 96, 61, 0.11);
844 
845 /* inline-painted surfaces/ink, retuned for daylight (match the CSS code/prose flips) */
846 --gx-well: #eef4f3; /* pale scope vellum — the readout well */
847 --gx-prose-ink: #2a3338;
849 
850/* atmosphere: on black the corner glows bloomed; on the daylit panel they
851 settle into soft cool tints, and the graticule reads as a printed grid */
852[data-theme='light'] .gx::after {
853 opacity: 0.42;
855[data-theme='light'] .gx-noise {
856 opacity: 0.05;
857 mix-blend-mode: multiply;
859 
860/* prose body ink (hardcoded literal, off-token) */
861[data-theme='light'] .gx-prose p {
862 color: #2a3338;
864 
865/* dropcap keeps the deepened signal cyan via --cyan */
866 
867/* code surfaces: pale scope vellum instead of near-black glass */
868[data-theme='light'] .gx-code {
869 background: #eef4f3;
870 color: #2a3338;
872[data-theme='light'] .gx-input {
873 background: #eef4f3;
875 
876/* range track sits on lighter line; thumb ring matches the panel bg via --bg */
877 
878/* callouts: retune the hardcoded borders + text for the daylit panel
879 (each text colour solved to clear AA on its own glow-tinted ground) */
880[data-theme='light'] .gx-callout.warn {
881 border-color: #d09b9b;
882 color: #8f2020;
884[data-theme='light'] .gx-callout.ok {
885 border-color: #7fbb9f;
886 color: #0d5d3c;
888[data-theme='light'] .gx-callout.info {
889 border-color: #7cc1b7;
890 color: #04534b;
892 
893/* coda block: on black the cyan glow bloomed off transparent; on the daylit
894 panel it settles into a soft cool tint that still reads against the vellum.
895 Border + kicker keep flipping via --line2 / --cyan tokens. */
896[data-theme='light'] .gx-coda {
897 background: linear-gradient(180deg, var(--cyan-glow), var(--panel));
899 
900/* rpc response bar/tag: amber gradient + border literals (deepened for paper) */
901[data-theme='light'] .gx .rpc-bar.am {
902 background: linear-gradient(var(--amber), #c79a52);
904[data-theme='light'] .gx .rpc-msg.res .rpc-tag {
905 border-color: #b98a3c;
⋯ 1 line hidden (lines 906–906)

Dedupe — acid-lab's two endings

Two lessons had competing closings. acid-lab carried a proper "Synthesis" section and, separately, a decorative footer in the composition root that read as a second, poetic close. The footer is removed (its couplet preserved as a colophon comment), and the Synthesis section becomes the sole ending by gaining the where-next beat it lacked — organized into two concept families rather than a bullet salad.

The new where-next, grouped by concept family, in the lesson's iso- classes.

src/lessons/acid-lab/sections/SynthesisSection.jsx · 427 lines
src/lessons/acid-lab/sections/SynthesisSection.jsx427 lines · JSX
⋯ 161 lines hidden (lines 1–161)
1import { forwardRef } from 'react';
2import { SectionDivider } from '../components/SectionDivider.jsx';
3import { SYNTHESIS_PHASES } from '../components/data.js';
4import { renderProseMarkdown } from '../components/helpers.js';
5 
6export const SynthesisSection = forwardRef(function SynthesisSection(_props, ref) {
7 return (
8 <>
9 <div ref={ref} style={{ scrollMarginTop: 16, marginTop: 32 }}>
10 <SectionDivider
11 letter="∴"
12 kicker="Putting it all together"
13 name="Synthesis"
14 accent="var(--ink)"
15 />
16 </div>
17 <SynthesisBody />
18 </>
19 );
20});
21 
22function SynthesisBody() {
23 return (
24 <div style={{ maxWidth: 880, margin: '0 auto', position: 'relative', zIndex: 2 }}>
25 <div className="iso-card" style={{ padding: '24px 28px', borderRadius: 12 }}>
26 <div
27 className="iso-ui"
28 style={{
29 fontSize: 10,
30 letterSpacing: '0.18em',
31 textTransform: 'uppercase',
32 color: 'rgba(var(--iso-ink-rgb), 0.72)',
33 marginBottom: 6,
34 }}
35 >
36 ACID is a coordinate system, not a checklist
37 </div>
38 <h3
39 className="iso-display"
40 style={{
41 fontSize: 22,
42 fontWeight: 500,
43 color: 'var(--ink)',
44 margin: 0,
45 fontStyle: 'italic',
46 }}
47 >
48 The lifecycle of one transaction
49 </h3>
50 
51 <div className="iso-rule-short" style={{ margin: '20px 0' }} />
52 
53 <p
54 className="iso-body"
55 style={{
56 fontSize: 16,
57 lineHeight: 1.65,
58 color: 'rgba(var(--iso-ink-rgb), 0.85)',
59 margin: '0 0 22px',
60 }}
61 dangerouslySetInnerHTML={{
62 __html: renderProseMarkdown(
63 "A single transaction's journey, end to end, exercises all four properties. Each phase has different letters doing different work. Understanding *which* property is active *when* is what makes ACID feel less like a memorized list and more like a coordinate system.",
64 ),
65 }}
66 />
67 
68 <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
69 {SYNTHESIS_PHASES.map((p, i) => (
70 <SynthesisPhase key={i} phase={p} isLast={i === SYNTHESIS_PHASES.length - 1} />
71 ))}
72 </div>
73 
74 <div
75 style={{
76 marginTop: 24,
77 padding: '16px 18px',
78 borderRadius: 8,
79 background: 'rgba(var(--iso-teal-rgb), 0.04)',
80 border: '1px solid rgba(var(--iso-teal-rgb), 0.2)',
81 }}
82 >
83 <div
84 className="iso-ui"
85 style={{
86 fontSize: 9,
87 letterSpacing: '0.16em',
88 textTransform: 'uppercase',
89 color: 'var(--iso-teal)',
90 marginBottom: 8,
91 fontWeight: 600,
92 }}
93 >
94 The takeaway
95 </div>
96 <p
97 className="iso-body"
98 style={{
99 fontSize: 15,
100 lineHeight: 1.6,
101 color: 'rgba(var(--iso-ink-rgb), 0.88)',
102 margin: 0,
103 }}
104 dangerouslySetInnerHTML={{
105 __html: renderProseMarkdown(
106 'A and I do not subsume each other. Atomicity protects you from *partial* transactions; isolation protects you from *interfering* ones. You can have a lost update at any isolation level (an isolation problem) on a perfectly atomic database. You can have a half-applied transaction after a crash (an atomicity problem) regardless of how strong your isolation is. Different axes, different machinery, different bugs when each fails.',
107 ),
108 }}
109 />
110 </div>
111 
112 <WhereToGoNext />
113 </div>
114 </div>
115 );
117 
118// Two concept families, each pointing outward from a single-node ACID database:
119// what it costs to keep the guarantees across machines, and what you trade them
120// for when you relax them. Each entry carries its own accent so the family reads
121// at a glance; the colours are the same flip-aware tokens the rest of the lesson
122// paints with, so the whole block themes for free in both modes.
123const NEXT_FAMILIES = [
124 {
125 label: 'Holding the line across machines',
126 blurb:
127 'Everything here assumed one box. Spread the data over many, and each letter has to be re-earned over a network that drops messages and partitions.',
128 accent: '--iso-teal',
129 items: [
130 [
131 'Distributed transactions',
132 'Two-phase commit and Saga patterns — how *all-or-nothing* survives when "all" is spread across several nodes that can each fail independently.',
133 ],
134 [
135 'Spanner & CockroachDB',
136 'Strict-serializable ACID *everywhere*, bought with synchronized clocks and Raft/Paxos consensus under every write. ACID as a global, not a local, contract.',
137 ],
138 ],
139 },
140 {
141 label: 'Trading the guarantees away',
142 blurb:
143 'Sometimes the full contract costs more than the workload can pay. These are the deliberate relaxations — and exactly which letter each one gives up.',
144 accent: '--iso-amber',
145 items: [
146 [
147 'Optimistic vs. pessimistic locking',
148 'Two ways to deliver Isolation: lock up front and serialize, or run free and abort on conflict at commit. The same I, two very different latency profiles.',
149 ],
150 [
151 'Eventual consistency & read repair',
152 'Drop linearizability for availability under partition (the CAP trade). Replicas reconcile after the fact instead of agreeing before the write returns.',
153 ],
154 [
155 'CRDTs',
156 'Conflict-free replicated data types: structure the data so concurrent writes *always* merge without coordination — convergence by algebra instead of by locks.',
157 ],
158 ],
159 },
160];
161 
162function WhereToGoNext() {
163 return (
164 <div style={{ marginTop: 36 }}>
165 <div className="iso-rule-short" style={{ margin: '0 0 18px' }} />
166 <div
167 className="iso-ui"
168 style={{
169 fontSize: 10,
170 letterSpacing: '0.18em',
171 textTransform: 'uppercase',
172 color: 'rgba(var(--iso-ink-rgb), 0.72)',
173 marginBottom: 6,
174 }}
175 >
176 Where to go next
177 </div>
178 <h3
179 className="iso-display"
180 style={{
181 fontSize: 22,
182 fontWeight: 500,
183 color: 'var(--ink)',
184 margin: 0,
185 fontStyle: 'italic',
186 }}
187 >
188 Four guarantees, and the price of keeping them elsewhere
189 </h3>
190 <p
191 className="iso-body"
192 style={{
193 fontSize: 15,
194 lineHeight: 1.6,
195 color: 'rgba(var(--iso-ink-rgb), 0.85)',
196 margin: '14px 0 22px',
197 }}
198 dangerouslySetInnerHTML={{
199 __html: renderProseMarkdown(
200 'ACID is the contract a *single* database makes. The interesting questions start when you ask what it costs to keep that contract across many machines, or which letter you are willing to give up to go faster.',
201 ),
202 }}
203 />
204 
205 <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
206 {NEXT_FAMILIES.map((family) => (
207 <NextFamily key={family.label} family={family} />
208 ))}
209 </div>
210 </div>
211 );
⋯ 215 lines hidden (lines 213–427)
213 
214function NextFamily({ family }) {
215 return (
216 <div
217 style={{
218 padding: '16px 18px',
219 borderRadius: 8,
220 background: `rgba(var(${family.accent}-rgb), 0.04)`,
221 border: `1px solid rgba(var(${family.accent}-rgb), 0.2)`,
222 }}
223 >
224 <div
225 className="iso-ui"
226 style={{
227 fontSize: 9,
228 letterSpacing: '0.16em',
229 textTransform: 'uppercase',
230 color: `var(${family.accent})`,
231 marginBottom: 8,
232 fontWeight: 600,
233 }}
234 >
235 {family.label}
236 </div>
237 <p
238 className="iso-body"
239 style={{
240 fontSize: 14,
241 lineHeight: 1.6,
242 color: 'rgba(var(--iso-ink-rgb), 0.85)',
243 margin: '0 0 14px',
244 fontStyle: 'italic',
245 }}
246 >
247 {family.blurb}
248 </p>
249 <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
250 {family.items.map(([topic, detail]) => (
251 <div
252 key={topic}
253 style={{
254 display: 'grid',
255 gridTemplateColumns: '6px 1fr',
256 alignItems: 'flex-start',
257 gap: 12,
258 }}
259 >
260 <div
261 style={{
262 width: 6,
263 height: 6,
264 borderRadius: '50%',
265 marginTop: 8,
266 background: `var(${family.accent})`,
267 flexShrink: 0,
268 }}
269 />
270 <div>
271 <span
272 className="iso-display"
273 style={{
274 fontSize: 16,
275 fontWeight: 600,
276 color: 'var(--ink)',
277 fontStyle: 'italic',
278 }}
279 >
280 {topic}
281 </span>
282 <p
283 className="iso-body"
284 style={{
285 fontSize: 14,
286 lineHeight: 1.55,
287 color: 'rgba(var(--iso-ink-rgb), 0.82)',
288 margin: '3px 0 0',
289 }}
290 dangerouslySetInnerHTML={{ __html: renderProseMarkdown(detail) }}
291 />
292 </div>
293 </div>
294 ))}
295 </div>
296 </div>
297 );
299 
300function SynthesisPhase({ phase, isLast }) {
301 const propColors = {
302 A: 'var(--iso-violet)',
303 C: 'var(--iso-pink)',
304 I: 'var(--iso-teal)',
305 D: 'var(--iso-amber)',
306 };
307 // Channel triples for the letter chip's wash + border, so they darken on
308 // paper in step with the text colours above (1f ≈ 0.12, 55 ≈ 0.33).
309 const propRgb = {
310 A: '--iso-violet-rgb',
311 C: '--iso-pink-rgb',
312 I: '--iso-teal-rgb',
313 D: '--iso-amber-rgb',
314 };
315 
316 return (
317 <div style={{ position: 'relative' }}>
318 <div
319 style={{
320 display: 'grid',
321 gridTemplateColumns: '120px 1fr',
322 alignItems: 'flex-start',
323 gap: 18,
324 padding: '14px 0',
325 }}
326 className="iso-synth-row"
327 >
328 <div style={{ paddingTop: 2 }}>
329 <div
330 className="iso-display"
331 style={{
332 fontSize: 16,
333 fontWeight: 600,
334 color: 'var(--ink)',
335 letterSpacing: '0.04em',
336 }}
337 >
338 {phase.phase}
339 </div>
340 <div
341 className="iso-body"
342 style={{
343 fontSize: 12,
344 color: 'rgba(var(--iso-ink-rgb), 0.72)',
345 marginTop: 4,
346 fontStyle: 'italic',
347 lineHeight: 1.4,
348 }}
349 >
350 {phase.detail}
351 </div>
352 </div>
353 <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
354 {phase.activeProps.length === 0 ? (
355 <div
356 className="iso-body"
357 style={{
358 fontSize: 13,
359 color: 'rgba(var(--iso-ink-rgb), 0.78)',
360 fontStyle: 'italic',
361 padding: '6px 0',
362 }}
363 >
364 The transaction is complete. No further work for any property.
365 </div>
366 ) : (
367 Object.entries(phase.annotations).map(([letter, note]) => (
368 <div
369 key={letter}
370 style={{
371 display: 'grid',
372 gridTemplateColumns: '34px 1fr',
373 alignItems: 'flex-start',
374 gap: 12,
375 }}
376 >
377 <div
378 style={{
379 width: 28,
380 height: 28,
381 borderRadius: 6,
382 background: `rgba(var(${propRgb[letter]}), 0.12)`,
383 border: `1px solid rgba(var(${propRgb[letter]}), 0.33)`,
384 display: 'flex',
385 alignItems: 'center',
386 justifyContent: 'center',
387 flexShrink: 0,
388 }}
389 >
390 <span
391 className="iso-display"
392 style={{
393 fontSize: 14,
394 fontWeight: 600,
395 color: propColors[letter],
396 }}
397 >
398 {letter}
399 </span>
400 </div>
401 <div
402 className="iso-body"
403 style={{
404 fontSize: 14,
405 lineHeight: 1.55,
406 color: 'rgba(var(--iso-ink-rgb), 0.85)',
407 }}
408 dangerouslySetInnerHTML={{ __html: renderProseMarkdown(note) }}
409 />
410 </div>
411 ))
412 )}
413 </div>
414 </div>
415 {!isLast && (
416 <div
417 style={{
418 height: 1,
419 background: 'rgba(var(--iso-ink-rgb), 0.06)',
420 marginLeft: 0,
421 marginRight: 0,
422 }}
423 />
424 )}
425 </div>
426 );

Dedupe — extracting bloom-filters' buried close

bloom-filters was the structural one. Its close was woven into the tail of chapter 10 ("When not to") plus a decorative footer — no distinct coda at all. The synthesis is lifted out into a real Coda section (a Chapter, num 11), the duplicate is removed from chapter 10, and the section is wired into the composition root and the table of contents.

The extracted close: the one idea, then four accurate onward pointers.

src/lessons/bloom-filters/sections/Coda.jsx · 63 lines
src/lessons/bloom-filters/sections/Coda.jsx63 lines · JSX
⋯ 31 lines hidden (lines 1–31)
1import { Chapter } from '../components/Chapter.jsx';
2 
3const LABEL_STYLE = {
4 fontSize: '0.72rem',
5 letterSpacing: '0.18em',
6 textTransform: 'uppercase',
7};
8 
9const NEXT = [
10 [
11 'The original paper.',
12 "Burton Bloom's 1970 note, “Space/Time Trade-offs in Hash Coding with Allowable Errors,” " +
13 'is four pages and still the clearest derivation of the bound you tuned in the math lab.',
14 ],
15 [
16 'The sketch family, formally.',
17 'HyperLogLog, Count-Min, and MinHash all trade exactness for bounded error in bounded space. ' +
18 'Read them as one toolkit and the membership filter stops looking like a special case.',
19 ],
20 [
21 'Filters inside an LSM tree.',
22 'Trace one point lookup through RocksDB or Cassandra: the filter is what lets a read skip whole ' +
23 'SSTables it knows it can ignore. The amplification math is where the savings become money.',
24 ],
25 [
26 'The succinct-structures frontier.',
27 'Quotient, cuckoo, and ribbon filters push toward the information-theoretic floor for ' +
28 'approximate membership. That floor, and how close each design sits to it, is the open question.',
29 ],
30];
31 
32export function Coda() {
33 return (
34 <Chapter num="11" title="Coda" anchor="coda">
35 <p className="bf-ui bf-mark-muted" style={{ ...LABEL_STYLE, marginBottom: '0.75rem' }}>
36 The one idea
37 </p>
38 <div className="bf-pullquote" style={{ marginTop: 0 }}>
39 A Bloom filter is never the answer to a question. It is a fast way to know you can skip{' '}
40 <em>asking</em> the real answer.
41 </div>
42 <p>
43 The real answer always lives somewhere else: in the SSTable, the cache, the database, the
44 source of truth. The filter only ever says <strong>definitely not</strong> &mdash; trust it,
45 skip the lookup &mdash; or <strong>probably yes</strong> &mdash; don't trust it, go check.
46 Hold that asymmetry in mind and every variant in the family falls into place. They are all,
47 every one of them, ways to skip work with one-sided error in bounded space.
48 </p>
49 
50 <p
51 className="bf-ui bf-mark-muted"
52 style={{ ...LABEL_STYLE, marginTop: '3rem', marginBottom: '0.75rem' }}
53 >
54 Where to go next
55 </p>
56 {NEXT.map(([title, body]) => (
57 <p key={title}>
58 <strong>{title}</strong> {body}
59 </p>
60 ))}
61 </Chapter>
62 );
⋯ 1 line hidden (lines 63–63)

Wired in order: Ch10 -> Coda -> Footer.

src/lessons/bloom-filters/BloomFiltersLesson.jsx · 90 lines
src/lessons/bloom-filters/BloomFiltersLesson.jsx90 lines · JSX
⋯ 82 lines hidden (lines 1–82)
1import './bloom-filters.css';
2 
3import { Footer } from './components/Footer.jsx';
4import { Hero } from './components/Hero.jsx';
5import { TableOfContents } from './components/TableOfContents.jsx';
6 
7import { BlockedBFVisual } from './labs/BlockedBFVisual.jsx';
8import { CountingBFDemo } from './labs/CountingBFDemo.jsx';
9import { MathLab } from './labs/MathLab.jsx';
10import { Sandbox } from './labs/Sandbox.jsx';
11import { SaturationDemo } from './labs/SaturationDemo.jsx';
12import { ScalableVisual } from './labs/ScalableVisual.jsx';
13 
14import { Ch01TheQuestion } from './sections/Ch01TheQuestion.jsx';
15import { Ch02TheConstruction } from './sections/Ch02TheConstruction.jsx';
16import { Ch03TheAsymmetry } from './sections/Ch03TheAsymmetry.jsx';
17import { Ch04TheMath } from './sections/Ch04TheMath.jsx';
18import { Ch04bImplementationNote } from './sections/Ch04bImplementationNote.jsx';
19import { Ch05Saturation } from './sections/Ch05Saturation.jsx';
20import { Ch06TheLimits } from './sections/Ch06TheLimits.jsx';
21import { Ch07TheVariants } from './sections/Ch07TheVariants.jsx';
22import { Ch07bBlocked } from './sections/Ch07bBlocked.jsx';
23import { Ch07cScalable } from './sections/Ch07cScalable.jsx';
24import { Ch07dRestOfFamily } from './sections/Ch07dRestOfFamily.jsx';
25import { Ch08TheCousins } from './sections/Ch08TheCousins.jsx';
26import { Ch09InProduction } from './sections/Ch09InProduction.jsx';
27import { Ch10WhenNotTo } from './sections/Ch10WhenNotTo.jsx';
28import { Coda } from './sections/Coda.jsx';
29 
30function LabFrame({ children }) {
31 return (
32 <div
33 className="max-w-4xl mx-auto px-6 md:px-12"
34 style={{ marginTop: '-2rem', marginBottom: '4rem' }}
35 >
36 {children}
37 </div>
38 );
40 
41export default function BloomFiltersLesson() {
42 return (
43 <div className="bf-root">
44 <TableOfContents />
45 <Hero />
46 
47 <Ch01TheQuestion />
48 <Ch02TheConstruction />
49 <LabFrame>
50 <Sandbox />
51 </LabFrame>
52 
53 <Ch03TheAsymmetry />
54 <Ch04TheMath />
55 <LabFrame>
56 <MathLab />
57 </LabFrame>
58 
59 <Ch04bImplementationNote />
60 <Ch05Saturation />
61 <LabFrame>
62 <SaturationDemo />
63 </LabFrame>
64 
65 <Ch06TheLimits />
66 <Ch07TheVariants />
67 <LabFrame>
68 <CountingBFDemo />
69 </LabFrame>
70 
71 <Ch07bBlocked />
72 <LabFrame>
73 <BlockedBFVisual />
74 </LabFrame>
75 
76 <Ch07cScalable />
77 <LabFrame>
78 <ScalableVisual />
79 </LabFrame>
80 
81 <Ch07dRestOfFamily />
82 <Ch08TheCousins />
83 <Ch09InProduction />
84 <Ch10WhenNotTo />
85 <Coda />
86 
87 <Footer />
88 </div>
⋯ 2 lines hidden (lines 89–90)
89 );

A bug the isolation scan caught

One agent's new comment in b-trees.css listed palette tokens as --card/--card-2/--rule/--ink-*/--oak-2/--stamp. The */ inside --ink-*/ silently closed the CSS comment early — turning the rest of the comment into an invalid selector that swallowed the .bt-coda rule, so the coda card would have rendered unstyled. CSS error-recovery hides this: no build error, just a missing rule. The style-isolation scan flagged it as a stray global selector, and the comment was reworded.

The reworded comment (no */ in the token list) and the now-applied .bt-coda rule.

src/lessons/b-trees/b-trees.css · 766 lines
src/lessons/b-trees/b-trees.css766 lines · CSS
⋯ 394 lines hidden (lines 1–394)
1@import url('https://fonts.googleapis.com/css2?family=Zilla+Slab:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Libre+Franklin:wght@300;400;500;600;700&display=swap');
2 
3.bt-root {
4 /* Warm linen card-stock, not flat beige: the ground carries a faint manila
5 warmth and the shadows go a shade cooler, so it reads as aged paper. The
6 ink ramp is three legible tiers of warm sepia — every tier clears AA on the
7 paper it sits on (ink-3 was retuned up from #938778 so readout labels and
8 footnotes stop dropping below 4.5:1). */
9 --paper: #e9e2d0;
10 --paper-2: #ddd4be;
11 --card: #f5f0e3;
12 --card-2: #ece5d3;
13 --ink: #2a241d;
14 --ink-2: #5b5244;
15 --ink-3: #6c6354;
16 --oak: #5c3f27;
17 --oak-2: #79573a;
18 --oak-deep: #4a3220;
19 --blue: #1c4d60;
20 --blue-2: #2c748c;
21 --blue-wash: rgba(28, 77, 96, 0.1);
22 --stamp: #b13627;
23 --stamp-2: #bd4330;
24 --stamp-wash: rgba(177, 54, 39, 0.1);
25 /* brass deepened from #9c7a35 so the kit callout label (small text on a card
26 ground) clears AA; brass-2 stays the brighter highlight stop (lamplit
27 hardware) for the slider end + knob radial */
28 --brass: #816225;
29 --brass-2: #b48c3e;
30 --rule: rgba(42, 36, 29, 0.17);
31 --rule-2: rgba(42, 36, 29, 0.31);
32 
33 /* tokens for colors also painted inline in JSX (SVG cell labels, chip
34 border tints) — defined in both modes so [data-theme] can flip them */
35 --bt-on-accent: #fff;
36 --bt-chip-stamp-border: rgba(177, 54, 39, 0.4);
37 --bt-chip-blue-border: rgba(28, 77, 96, 0.4);
38 
39 /* lesson-kit token contract — maps the kit to this light card-catalog palette.
40 Only the Callout is adopted from the kit (see components/Callout.jsx); the
41 bt-read/bt-chip/bt-slider widgets stay bespoke. */
42 --lk-accent: var(--brass);
43 --lk-accent-tint: rgba(129, 98, 37, 0.1);
44 --lk-ink: var(--ink);
45 --lk-ink-dim: var(--ink-2);
46 --lk-ink-faint: var(--ink-3);
47 --lk-panel: var(--card);
48 --lk-rule: var(--rule);
49 --lk-radius: 4px;
50 --lk-display: 'Zilla Slab', serif;
51 --lk-callout-pad: 12px 16px;
52 --lk-callout-radius: 0 4px 4px 0;
53 --lk-callout-body-size: 14.5px;
54 --lk-callout-body-color: var(--ink);
55 
56 /* atmosphere: a wash of warm reading-lamp light pooling from the top and a
57 soft cooler settling at the foot, so the cream reads as a lit page on an
58 oak desk rather than a flat fill. Flipped in dark to a lamplit pool over
59 near-black. */
60 --bt-glow-warm: rgba(214, 180, 120, 0.22);
61 --bt-glow-edge: rgba(96, 78, 52, 0.07);
62 background:
63 radial-gradient(120% 70% at 50% -8%, var(--bt-glow-warm), transparent 60%),
64 radial-gradient(125% 80% at 50% 112%, var(--bt-glow-edge), transparent 55%), var(--paper);
65 background-attachment: fixed;
66 color: var(--ink);
67 font-family: 'Libre Franklin', system-ui, sans-serif;
68 font-weight: 400;
69 line-height: 1.66;
70 letter-spacing: 0.005em;
71 position: relative;
72 overflow-x: hidden;
73 -webkit-font-smoothing: antialiased;
75.bt-root * {
76 box-sizing: border-box;
78.bt-display {
79 font-family: 'Zilla Slab', serif;
81.bt-mono {
82 font-family: var(--font-mono);
84 
85/* paper grain + faint horizontal ruling, like aged ruled card stock: a fine
86 fractal speckle over a whisper of sepia catalog-card lines */
87.bt-grain {
88 position: fixed;
89 inset: 0;
90 pointer-events: none;
91 z-index: 1;
92 opacity: 0.55;
93 background-image:
94 repeating-linear-gradient(
95 var(--bt-rule-line, rgba(74, 50, 32, 0.05)) 0 1px,
96 transparent 1px 29px
97 ),
98 url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.4'/%3E%3C/svg%3E");
99 mix-blend-mode: multiply;
101.bt-progress {
102 position: fixed;
103 top: 0;
104 left: 0;
105 height: 3px;
106 background: var(--stamp);
107 z-index: 50;
108 transition: width 0.1s linear;
110 
111.bt-wrap {
112 position: relative;
113 z-index: 2;
114 max-width: 880px;
115 margin: 0 auto;
116 padding: 0 22px;
118.bt-section {
119 padding: 54px 0;
120 border-top: 1px solid var(--rule);
122.bt-section:first-of-type {
123 border-top: none;
125 
126.bt-kicker {
127 font-family: var(--font-mono);
128 font-size: 11.5px;
129 letter-spacing: 0.28em;
130 text-transform: uppercase;
131 color: var(--stamp);
132 font-weight: 700;
134.bt-roman {
135 font-family: 'Zilla Slab', serif;
136 font-size: 13px;
137 font-weight: 600;
138 letter-spacing: 0.18em;
139 color: var(--oak-2);
141.bt-h2 {
142 font-family: 'Zilla Slab', serif;
143 font-weight: 600;
144 font-size: clamp(27px, 5.4vw, 40px);
145 line-height: 1.08;
146 letter-spacing: -0.01em;
147 margin: 0.32em 0 0.1em;
148 color: var(--ink);
150.bt-lead {
151 font-size: clamp(16px, 2.3vw, 18.5px);
152 color: var(--ink);
153 margin: 0.7em 0;
155.bt-p {
156 font-size: 16px;
157 color: var(--ink);
158 margin: 0.95em 0;
160.bt-p strong,
161.bt-lead strong {
162 font-weight: 600;
163 color: #1c1812;
165.bt-em {
166 font-style: italic;
168.bt-blue {
169 color: var(--blue);
170 font-weight: 600;
172.bt-stampc {
173 color: var(--stamp);
174 font-weight: 600;
176.bt-mono-in {
177 font-family: var(--font-mono);
178 background: var(--card);
179 border: 1px solid var(--rule);
180 border-radius: 4px;
181 padding: 0 5px;
182 font-size: 0.92em;
184 
185/* index-card surface */
186.bt-card {
187 background: var(--card);
188 border: 1px solid var(--rule);
189 border-radius: 3px;
190 box-shadow:
191 0 1px 0 rgba(255, 255, 255, 0.5) inset,
192 0 2px 7px rgba(42, 36, 29, 0.07),
193 0 14px 30px -22px rgba(42, 36, 29, 0.45);
194 position: relative;
196 
197/* a lab is framed like a catalog drawer with a guide-card tab */
198.bt-lab {
199 position: relative;
200 margin: 30px 0 8px;
201 background: linear-gradient(180deg, var(--card), var(--card-2));
202 border: 1px solid var(--rule-2);
203 border-radius: 5px;
204 box-shadow:
205 0 2px 8px rgba(42, 36, 29, 0.08),
206 0 22px 44px -30px rgba(42, 36, 29, 0.5);
208.bt-lab-tab {
209 display: inline-flex;
210 align-items: center;
211 gap: 7px;
212 position: absolute;
213 top: -13px;
214 left: 18px;
215 background: var(--oak);
216 color: #f3e9d8;
217 font-family: var(--font-mono);
218 font-size: 11px;
219 letter-spacing: 0.16em;
220 text-transform: uppercase;
221 font-weight: 700;
222 padding: 5px 12px;
223 border-radius: 4px;
224 box-shadow: 0 3px 8px -3px rgba(42, 36, 29, 0.6);
226.bt-lab-tab svg {
227 width: 13px;
228 height: 13px;
230.bt-lab-body {
231 padding: 26px 18px 18px;
233.bt-lab-cap {
234 font-size: 13px;
235 color: var(--ink-2);
236 font-style: italic;
237 margin-top: 12px;
238 min-height: 2.2em;
239 line-height: 1.5;
241 
242/* controls */
243.bt-controls {
244 display: flex;
245 flex-wrap: wrap;
246 gap: 8px;
247 align-items: center;
249.bt-btn {
250 font-family: 'Libre Franklin', sans-serif;
251 font-size: 13px;
252 font-weight: 600;
253 border: 1px solid var(--rule-2);
254 background: var(--paper);
255 color: var(--ink);
256 padding: 8px 13px;
257 border-radius: 5px;
258 cursor: pointer;
259 letter-spacing: 0.01em;
260 display: inline-flex;
261 align-items: center;
262 gap: 6px;
263 transition: all 0.14s ease;
264 user-select: none;
266.bt-btn:hover {
267 border-color: var(--oak-2);
268 background: var(--card);
270.bt-btn:active {
271 transform: translateY(1px);
273.bt-btn:disabled {
274 opacity: 0.4;
275 cursor: not-allowed;
277.bt-btn svg {
278 width: 15px;
279 height: 15px;
281.bt-btn-stamp {
282 background: var(--stamp);
283 color: #fbefe6;
284 border-color: var(--stamp);
286.bt-btn-stamp:hover {
287 background: var(--stamp-2);
288 border-color: var(--stamp-2);
290.bt-btn-blue {
291 background: var(--blue);
292 color: #eaf3f5;
293 border-color: var(--blue);
295.bt-btn-blue:hover {
296 background: var(--blue-2);
297 border-color: var(--blue-2);
299.bt-btn-ghost {
300 background: transparent;
302 
303.bt-keypill {
304 font-family: var(--font-mono);
305 font-weight: 700;
306 font-size: 13px;
307 border: 1px solid var(--rule-2);
308 background: var(--card);
309 color: var(--ink);
310 width: 38px;
311 height: 34px;
312 border-radius: 5px;
313 cursor: pointer;
314 transition: all 0.12s ease;
316.bt-keypill:hover {
317 border-color: var(--blue);
318 color: var(--blue);
320.bt-keypill:disabled {
321 opacity: 0.32;
322 cursor: not-allowed;
324 
325.bt-slider {
326 -webkit-appearance: none;
327 appearance: none;
328 width: 100%;
329 height: 5px;
330 border-radius: 3px;
331 background: linear-gradient(90deg, var(--oak) 0%, var(--brass) 100%);
332 outline: none;
334.bt-slider::-webkit-slider-thumb {
335 -webkit-appearance: none;
336 appearance: none;
337 width: 22px;
338 height: 22px;
339 border-radius: 50%;
340 background: var(--card);
341 border: 3px solid var(--stamp);
342 cursor: pointer;
343 box-shadow: 0 2px 6px -1px rgba(42, 36, 29, 0.5);
345.bt-slider::-moz-range-thumb {
346 width: 22px;
347 height: 22px;
348 border-radius: 50%;
349 background: var(--card);
350 border: 3px solid var(--stamp);
351 cursor: pointer;
353 
354/* readouts */
355.bt-readgrid {
356 display: grid;
357 grid-template-columns: repeat(3, 1fr);
358 gap: 10px;
360.bt-read {
361 background: var(--paper);
362 border: 1px solid var(--rule);
363 border-radius: 5px;
364 padding: 11px 12px;
365 text-align: center;
367.bt-read-n {
368 font-family: 'Zilla Slab', serif;
369 font-weight: 700;
370 font-size: clamp(22px, 5vw, 30px);
371 line-height: 1;
372 color: var(--ink);
373 transition: color 0.2s;
375.bt-read-l {
376 font-family: var(--font-mono);
377 font-size: 10px;
378 letter-spacing: 0.1em;
379 text-transform: uppercase;
380 color: var(--ink-3);
381 margin-top: 6px;
383 
384/* Callout: structure + most styling come from the lesson kit via the --lk-*
385 tokens above. These two rules pin the few bits the kit doesn't tokenize:
386 the label's bold weight + 4px gap, and the italic body. */
387.bt-root .lk-callout-label {
388 font-weight: 700;
389 margin-bottom: 4px;
391.bt-root .lk-callout-body {
392 font-style: italic;
394 
395/* Coda (section IX) — the "one idea" synthesis card + the numbered "where to go
396 next" list. Every property reads a palette token (card, rule, ink, oak,
397 stamp), all of which the dark block redefines, so both modes follow the
398 card-catalog palette for free — same pattern as the .bt-card surface above. */
399.bt-coda {
400 background: linear-gradient(180deg, var(--card), var(--card-2));
401 border: 1px solid var(--rule);
402 border-left: 3px solid var(--brass);
403 border-radius: 4px;
404 padding: 18px 20px;
405 margin: 6px 0 4px;
⋯ 360 lines hidden (lines 407–766)
407.bt-coda-lead {
408 margin: 8px 0 0;
409 max-width: 640px;
411.bt-coda-next-label {
412 display: block;
413 margin-top: 30px;
414 margin-bottom: 12px;
416.bt-next {
417 display: grid;
418 gap: 9px;
420.bt-next-item {
421 display: flex;
422 gap: 12px;
423 align-items: flex-start;
424 padding: 12px 14px;
425 border: 1px solid var(--rule);
426 border-radius: 5px;
427 background: var(--card);
429.bt-next-n {
430 font-family: var(--font-mono);
431 font-size: 11px;
432 font-weight: 700;
433 letter-spacing: 0.06em;
434 color: var(--oak-2);
435 min-width: 18px;
436 padding-top: 3px;
438.bt-next-t {
439 display: block;
440 font-family: 'Zilla Slab', serif;
441 font-weight: 600;
442 font-size: 15.5px;
443 color: var(--ink);
445.bt-next-d {
446 font-size: 14px;
447 color: var(--ink-2);
448 margin: 3px 0 0;
449 line-height: 1.55;
451 
452.bt-chip {
453 display: inline-flex;
454 align-items: center;
455 gap: 5px;
456 font-family: var(--font-mono);
457 font-size: 11px;
458 font-weight: 700;
459 letter-spacing: 0.04em;
460 padding: 3px 9px;
461 border-radius: 20px;
462 border: 1px solid var(--rule-2);
464.bt-chip-blue {
465 color: var(--blue);
466 border-color: var(--bt-chip-blue-border);
467 background: var(--blue-wash);
469.bt-chip-stamp {
470 color: var(--stamp);
471 border-color: var(--bt-chip-stamp-border);
472 background: var(--stamp-wash);
474.bt-chip-ink {
475 color: var(--ink-2);
477 
478/* reveal on scroll */
479.bt-rev {
480 opacity: 0;
481 transform: translateY(20px);
482 transition:
483 opacity 0.7s cubic-bezier(0.2, 0.7, 0.2, 1),
484 transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
486.bt-rev.in {
487 opacity: 1;
488 transform: none;
490 
491@keyframes btpulse {
492 0%,
493 100% {
494 opacity: 1;
495 transform: scale(1);
496 }
497 50% {
498 opacity: 0.6;
499 transform: scale(1.06);
500 }
502@keyframes btflash {
503 0% {
504 background: var(--stamp-wash);
505 }
506 100% {
507 background: transparent;
508 }
510 
511/* leaf-chain strip for the B+ range lab */
512.bt-strip {
513 display: flex;
514 align-items: stretch;
515 gap: 0;
516 overflow-x: auto;
517 padding: 6px 2px 12px;
519.bt-leafcard {
520 flex: 0 0 auto;
521 background: var(--card);
522 border: 1.5px solid var(--rule-2);
523 border-radius: 5px;
524 padding: 8px 6px;
525 min-width: 54px;
526 text-align: center;
527 transition: all 0.25s ease;
529.bt-leafcard.scan {
530 border-color: var(--blue);
531 background: var(--blue-wash);
532 box-shadow: 0 0 0 3px var(--blue-wash);
534.bt-leafarrow {
535 flex: 0 0 auto;
536 display: flex;
537 align-items: center;
538 color: var(--ink-3);
539 padding: 0 3px;
540 transition: color 0.25s;
542.bt-leafarrow.scan {
543 color: var(--blue);
545.bt-leafkey {
546 font-family: var(--font-mono);
547 font-weight: 700;
548 font-size: 13px;
549 padding: 2px 0;
550 border-radius: 3px;
551 transition: all 0.2s;
553.bt-leafkey.hit {
554 background: var(--stamp);
555 color: #fff;
557 
558/* hero cabinet */
559.bt-hero {
560 padding-top: 46px;
562.bt-cabinet {
563 display: grid;
564 grid-template-columns: repeat(3, 1fr);
565 gap: 9px;
566 max-width: 330px;
567 margin: 0 auto;
568 background: linear-gradient(180deg, var(--oak-2), var(--oak));
569 padding: 13px;
570 border-radius: 7px;
571 box-shadow:
572 0 3px 0 var(--oak-deep),
573 0 30px 60px -30px rgba(42, 36, 29, 0.85),
574 inset 0 1px 1px rgba(255, 255, 255, 0.12);
576.bt-drawer {
577 background: linear-gradient(180deg, #caa877, #b6915f);
578 border-radius: 4px;
579 padding: 9px 7px 11px;
580 position: relative;
581 box-shadow:
582 inset 0 1px 0 rgba(255, 255, 255, 0.4),
583 inset 0 -2px 3px rgba(74, 50, 32, 0.35);
584 transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
586.bt-drawer.open {
587 transform: translateY(-7px);
588 box-shadow:
589 inset 0 1px 0 rgba(255, 255, 255, 0.5),
590 0 0 14px -5px var(--blue-wash),
591 0 10px 18px -8px rgba(42, 36, 29, 0.6);
593.bt-drawer-label {
594 font-family: var(--font-mono);
595 font-size: 10px;
596 font-weight: 700;
597 letter-spacing: 0.06em;
598 color: #4a3220;
599 text-align: center;
600 background: #f0e7d5;
601 border-radius: 2px;
602 padding: 2px 0;
603 margin-bottom: 8px;
604 box-shadow: inset 0 0 0 1px rgba(74, 50, 32, 0.2);
606.bt-knob {
607 width: 11px;
608 height: 11px;
609 border-radius: 50%;
610 background: radial-gradient(circle at 35% 30%, var(--brass-2), #6e5320);
611 margin: 0 auto;
612 box-shadow: 0 1px 2px rgba(42, 36, 29, 0.6);
614 
615@media (max-width: 760px) {
616 .bt-wrap {
617 padding: 0 16px;
618 }
619 .bt-section {
620 padding: 42px 0;
621 }
622 .bt-readgrid {
623 gap: 7px;
624 }
625 .bt-read {
626 padding: 9px 6px;
627 }
628 .bt-lab-body {
629 padding: 24px 12px 16px;
630 }
631 .bt-grid-2 {
632 grid-template-columns: 1fr !important;
633 }
635 
636/* ─── Dark theme — complementary to the light card-catalog default ───
637 The reading room after hours: warm near-black drawers, lamplit oak, and
638 petrol-blue guide tabs glowing under brass. Nearly the whole design cascades
639 from the palette tokens below; the rules after them only retune the
640 hardcoded literals (gradient stops, white insets, paper-grain blend, and the
641 drawer hardware) that don't flow through a token. */
642[data-theme='dark'] .bt-root {
643 /* The reading room after hours. The drawers go warm near-black, the oak is
644 lamplit (lifted a step so the guide-tab text and wood-grain read as wood,
645 not mud), and ink-3 is raised to a legible-faint sepia so readout labels
646 and footnotes clear AA. Petrol-blue keeps its glow as the signature accent;
647 brass-warm hardware catches the lamp. */
648 --paper: #1d1812;
649 --paper-2: #14110b;
650 --card: #2c271d;
651 --card-2: #352e22;
652 --ink: #efe6d4;
653 --ink-2: #b8ad96;
654 --ink-3: #9c9177;
655 --oak: #b58f62;
656 --oak-2: #cea273;
657 --oak-deep: #6f4f31;
658 --blue: #5fb4d6;
659 --blue-2: #8ccdea;
660 --blue-wash: rgba(95, 180, 214, 0.17);
661 --stamp: #e07a5f;
662 --stamp-2: #ee977e;
663 --stamp-wash: rgba(224, 122, 95, 0.17);
664 --brass: #cda44d;
665 --brass-2: #e2bf63;
666 --rule: rgba(239, 230, 212, 0.16);
667 --rule-2: rgba(239, 230, 212, 0.31);
668 
669 /* dark ink reads better than white on the lightened accent fills; chip
670 border tints track the retuned petrol + stamp hues */
671 --bt-on-accent: #241710;
672 --bt-chip-stamp-border: rgba(224, 122, 95, 0.46);
673 --bt-chip-blue-border: rgba(95, 180, 214, 0.46);
674 
675 --lk-accent-tint: rgba(205, 164, 77, 0.15);
676 
677 /* a pool of lamplight from above warms the near-black; the foot settles back
678 into shadow. The fixed warm tint mirrors the light mode's reading lamp. */
679 --bt-glow-warm: rgba(214, 158, 86, 0.1);
680 --bt-glow-edge: rgba(0, 0, 0, 0.32);
681 --bt-rule-line: rgba(239, 230, 212, 0.035);
683 
684/* brighten the strong-text literal that doesn't read off a token */
685[data-theme='dark'] .bt-root .bt-p strong,
686[data-theme='dark'] .bt-root .bt-lead strong {
687 color: #fbf3e2;
689 
690/* paper grain: multiply vanishes on a dark ground, so lift it to screen and
691 dial it back to a faint phosphor speckle */
692[data-theme='dark'] .bt-root .bt-grain {
693 opacity: 0.16;
694 mix-blend-mode: screen;
696 
697/* index-card surface: kill the bright top-edge inset, deepen the cast shadow */
698[data-theme='dark'] .bt-root .bt-card {
699 box-shadow:
700 0 1px 0 rgba(255, 255, 255, 0.04) inset,
701 0 2px 7px rgba(0, 0, 0, 0.4),
702 0 14px 30px -22px rgba(0, 0, 0, 0.8);
704[data-theme='dark'] .bt-root .bt-lab {
705 box-shadow:
706 0 2px 8px rgba(0, 0, 0, 0.42),
707 0 22px 44px -30px rgba(0, 0, 0, 0.85);
709[data-theme='dark'] .bt-root .bt-lab-tab {
710 color: #211d17;
711 box-shadow: 0 3px 8px -3px rgba(0, 0, 0, 0.7);
713 
714/* button label colors: keep crisp text on the now-lighter accent fills */
715[data-theme='dark'] .bt-root .bt-btn-stamp {
716 color: #2a1a13;
718[data-theme='dark'] .bt-root .bt-btn-blue {
719 color: #0f2630;
721 
722[data-theme='dark'] .bt-root .bt-slider::-webkit-slider-thumb {
723 box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.6);
725 
726/* (chip tint borders track the retuned petrol + stamp hues automatically: the
727 base .bt-chip rules read --bt-chip-*-border, which the token block above
728 redefines for dark) */
729 
730/* leaf-key hit + median/cmp cells: dark ink reads better than white on the
731 lighter accents */
732[data-theme='dark'] .bt-root .bt-leafkey.hit {
733 color: #2a1a13;
735 
736/* hero cabinet: a darker oak carcass with brass-warm drawers */
737[data-theme='dark'] .bt-root .bt-cabinet {
738 box-shadow:
739 0 3px 0 var(--oak-deep),
740 0 30px 60px -30px rgba(0, 0, 0, 0.9),
741 inset 0 1px 1px rgba(255, 255, 255, 0.08);
743[data-theme='dark'] .bt-root .bt-drawer {
744 background: linear-gradient(180deg, #936e45, #6f5234);
745 box-shadow:
746 inset 0 1px 0 rgba(255, 240, 214, 0.2),
747 inset 0 -2px 3px rgba(0, 0, 0, 0.45);
749/* the pulled drawer catches the reading lamp: a warm top sheen and a soft
750 petrol halo, echoing the glowing guide tabs */
751[data-theme='dark'] .bt-root .bt-drawer.open {
752 background: linear-gradient(180deg, #a47b4d, #7a5a39);
753 box-shadow:
754 inset 0 1px 0 rgba(255, 240, 214, 0.3),
755 0 0 16px -4px var(--blue-wash),
756 0 10px 18px -8px rgba(0, 0, 0, 0.7);
758[data-theme='dark'] .bt-root .bt-drawer-label {
759 color: #2c2014;
760 background: #d8c39a;
761 box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.28);
763[data-theme='dark'] .bt-root .bt-knob {
764 background: radial-gradient(circle at 35% 30%, var(--brass-2), #4a3520);
765 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.7);