Phase 22.2 — The Crew's Toolkit
05 JUL AT 08:37 PM

Phase 22.2 — The Crew's Toolkit

0 LOVES 0 VIEWS
The reusable components.

UTS Phase 22.2 — Flutter shared components (the reusable guide atoms/molecules)

The reuse layer. 22.1 laid the tokens; 22.2 restyles the shared atoms/molecules every post-body block (22.3) and the lightbox (22.4/22.5) compose. Get these right → 22.3/22.4 become layout-only. Every component is specified from THREE sources, each with file:line — nothing is guessed, nothing is deferred to impl: - Look/valuesstatic/css/kon_guide_v2.css (the sole guide stylesheet) — the web is the reference. - Markup/behavior ← the kontent templates (…/templates/partials/{molecules,organisms}/…). - The Flutter pattern to follow ← the existing app idiom (accordion_block.dart disclosure, media_block.dart media-type switch, the 22.1 token + colorScheme.primary convention). We upgrade existing atoms / port missing web molecules — we do not invent mechanisms. Where CSS and any summary differ, the CSS wins. Every "translation" (CSS→Flutter) is named explicitly below.

Anti-rogue discipline (as 22.1): per component — re-open its cited web source + the app pattern → port → independent code-vs-source audit. New Color.lerp only via 22.1's gBrandInk/gBrandTint. No new architecture.

Program: Under the Tuscan Sun → Phase 22 → builds on 22.1 tokens. Owner: Flutter (with Travis).

Goal

Restyle the seven shared guide components onto 22.1 g-* tokens — kill the per-type rainbow + decorative icons, and add the two missing molecules the web already has (o_mat_gallery "Show N" disclosure, o_mat_thumb type-aware thumb). These are the atoms 22.3 (blocks) and 22.4/22.5 (lightbox) assemble; 22.2 lands visible changes wherever the atoms are already used.

Scope

In: the 7 components below. Each reads 22.1 g-* tokens; brand via colorScheme.primary. Out: block layout/composition (step_block/material_*/*_collection recompose → 22.3); the lightbox shell (22.4/22.5); deleting the old guide* rainbow consts (→ 22.6, after every consumer migrates). Atoms keep their public API where possible so 22.3 is a near-no-touch recompose (the disc and the strip change signature — listed).

Patterns this phase follows (so we upgrade, not reinvent) — grounded

Need The existing app pattern to follow (cited) The web rule it satisfies
Expand/collapse (gallery "Show N", section) accordion_block.dart:106-167StatefulWidget + open-state + InkWell header + AnimatedRotation(BootstrapIcons.chevron_down, color: colorScheme.primary) + ClipRect(AnimatedSize(200ms, easeInOut, isOpen ? body : SizedBox(height:0))) <details> 0fr→1fr transition + rotating chevron (.g-gal/.g-acc, CSS:246-257,220-226)
Render media by type (thumb/tile glyph) media_block.dart:27switch (media.mediaType) over image/video/audio/document; BootstrapIcons for glyphs _mat_thumb.gohtml Mime-prefix branches
Brand / tokens 22.1: static const + Theme.of(context).colorScheme.primary + gBrandInk/gBrandTint (no ThemeExtension) var(--g-*)
§5C block chrome content_block_renderer.dart:41-81 + block_wrapper.dart already render hidden/heading/showDivider the web block dispatcher

The component ports (the spine) — all resolved from source

Web. _mat_gallery.gohtml:9-11<details class="g-gal"><summary>Show {{len Items}} <chevron/></summary><div class="g-gal-reveal"><div><div class="g-strip">tiles</div></div></div></details> (native <details>, no JS). CSS: .g-gal (kon_guide_v2.css:245-257) collapse→reveal grid 0fr→1fr .28s; summary 11px/w600/gBrandInk + 9×6 chevron rotate(180deg) on [open] (:248-251); .g-strip (:230-232) horizontal, gap:6, snap, tiles 64×64(gTile)/gRadius; .g-tile mixed-media (:234-239): image cover / video poster+.g-tile-play (play triangle) / audio poster+waveform / audio-doc .g-tile-ic glyph on gTint; .g-tile-tag 8px label. App pattern. accordion_block.dart disclosure (table above). Current Flutter. guide_thumbnail_strip.dart — always-open ListView, all-images, old tokens. Target. GuideGalleryDisclosure(collection): - collapsed by default; header InkWellShow <N> (11px/w600, DesignTokens.gBrandInk(colorScheme.primary)) + AnimatedRotation(turns: open?0.5:0, BootstrapIcons.chevron_down, color: gBrandInk(...)). - reveal via ClipRect(AnimatedSize(200ms easeInOut)) (the app idiom = the web's 0fr→1fr). - strip: horizontal scroll, gTile(64) tiles, gap 6, gRadius; tile by media.mediaType (switch, like media_block): image=cover; video=poster + BootstrapIcons.play_fill overlay; audio=poster + BootstrapIcons.soundwave overlay; document=gTint + doc icon; tap → existing viewer (reuse the strip's current openGalleryLightbox routing). - Replaces GuideThumbnailStrip at all 9 call sites (same collection: arg; the per-call thumbSize: is dropped — tiles are fixed gTile). Sites listed under Consumers.

22.2-B — Media-type thumb → new MaterialThumb molecule (the web's o_mat_thumb, which Flutter lacks)

Web. _mat_thumb.gohtml (full) + .g-mat-thumb (kon_guide_v2.css:75-80): 48(gThumb) square, gTint, gRadius; type-aware: image=thumbnail cover; video=poster + 20px play triangle (.g-tile-play); audio=poster + waveform; document=.g-tile-ic bi icon; placeholder .g-mat-thumb--ph when no media; .g-mat-gal count badge bottom-right (rgba(0,0,0,.62)/white 9px). Tappable → shared viewer; no inline player. App pattern. media_block.dart:27 switch (media.mediaType) + BootstrapIcons. Current Flutter. No shared thumb — material_row.dart:42-60 inlines an image-only thumb and renders non-image media as a full inline MediaBlock below (:129-140). The web is image-light (thumb only). Target. MaterialThumb(media, galleryCount)48(gThumb)/gTint/gRadius; switch (media.mediaType): image=cover; video=poster + BootstrapIcons.play_fill; audio=poster + BootstrapIcons.soundwave; document= BootstrapIcons.file_earmark*; none=placeholder (image glyph on gTint); + count badge bottom-right; tap → shared viewer. (material_row drops its inline players + adopts this in 22.3.)

22.2-C — Section (all-open by default) → new GuideSection molecule

Web. .g-acc (kon_guide_v2.css:220-226), used in _block_material_collection.gohtml:23 (<details class="g-acc" open><summary class="g-acc-sum"><span>{group|Materials}</span></summary><div class="g-acc-body">). .g-acc-sum = gTint bar / gRadius / 13px w700 UPPERCASE gInk / space-between; trailing ::after = open / + closed, gBrandInk 18px; .g-acc-body pad .25rem 0 .7rem. App pattern. accordion_block.dart (StatefulWidget + AnimatedSize), but initiallyExpanded: true (Q3 all-open). Current Flutter. None (the generic accordion_block.dart is a different surface — HTML content blocks). Target. GuideSection(title, child) — open by default; header bar gTint/gRadius/13px w700 uppercase gInk, trailing /+ text glyph in gBrandInk(colorScheme.primary) (the .g-acc-sum::after); body pad .25/0/.7rem. Sole consumer = material_collection_block (22.3).

22.2-D — Step disc (glyph-by-type) → new StepDisc atom (replaces timeline_circle.dart)

Web. .g-step-num (kon_guide_v2.css:91-93,103,108-110): 30px(gDisc), bg:var(--g-brand), color:var(--g-on-brand), 13px/w600, no ring; --donegOk+ (glyph hidden, :108-110); --wronggBad. Glyph by context (templates): instruction=3-bar svg (_block_step.gohtml:9); question=? (:29); collection/lesson=ordinal N (_block_step_collection.gohtml:39, _guide_lightbox.gohtml:291). Current Flutter. timeline_circle.dart24px, per-state rainbow halo ring, always number. Target. StepDisc({content: StepDiscContent, state: StepDiscState})30(gDisc), no halo; bg = colorScheme.primary (default) / gOk (done) / gBad (wrong); white text 13px w600. content = .bars (3 rounded Containers, widths ~10/10/7 → matches the 3 <rect>s; drawn widgets, not SVG/CustomPaint) / .question ("?") / .number(n). state.donegOk + BootstrapIcons.check (or ). Consumers: replaces TimelineCircle in step_collection_block (ordinal) and is newly added to the standalone step in 22.3 (3-bar / ?), where the colored StepBadgeRow type badge is dropped (the disc carries the type).

22.2-E — Neutralized labels (de-rainbow)

  • Material type.g-mat-type (kon_guide_v2.css:83): 11px / gFaint plain text, "not a colored badge."MaterialTypeBadge (material_type_badge.dart:42-69, 6-color switch) → plain gFaint 11px text (no bg).
  • Optional.g-mat-opt (:84): 10px UPPERCASE gFaint, 1px gLine2 border, radius 3, pad 0 5. → OptionalBadge (material_type_badge.dart:73-103, red/pink) → bordered faint pill.
  • Question sub-type.g-qtype (:139): 11px / gFaint, lowercase multiple choice / true / false / flashcard (_block_step.gohtml:35). → StepBadgeRow._StepBadge.fromQuestionType (rainbow) → neutral gFaint lowercase label.
  • Step type (INSTRUCTION/QUESTION) — the web shows no type text badge (disc glyph carries it). → remove _StepBadge.fromType from step_badge.dart.
  • Duration.g-step-dur (:97): 10px gMuted, gTint bg, pad 1px 7px, radius 10, no icon. → the step_badge.dart duration badge (BootstrapIcons.clock, :34-39) → de-iconed neutral muted pill.

22.2-F — De-iconed tip / explanation

  • Tip.g-tip (kon_guide_v2.css:99): 13px, gWarnInk on gWarnBg, border-left:3px gWarnBd, pad .5/.7rem, radius 0 gRadius gRadius 0, no icon. → tip_box.dart drops BootstrapIcons.lightbulb (:39-43).
  • Explanation.g-explain (:100): 13px, gBody on gTint, border-left:3px var(--g-brand), same pad/radius, no icon. → explanation_box.dart drops BootstrapIcons.info_circle (:45-49); left border = colorScheme.primary.
  • (Functional icons STAY — answer-option bi-circle/bi-check-circle-fill _block_step.gohtml:99,105, flashcard reveal bi-eye :79; only the decorative tip/explain icons go.)

22.2-G — §5C block chrome → verified already correct (no change)

content_block_renderer.dart:41-81 renders hidden (:41-47) and wraps every block in BlockWrapper(heading, showDivider); block_wrapper.dart:23-44 renders the heading + the divider; sort_order is the caller's list order. The heading uses generic block chrome (blockH2Size), not guide g- tokens — correct (it's shared block chrome, not a guide surface). No gap; verify-only — no edit.


CSS→Flutter translations (documented, not 1:1 — like 22.1's color-mixColor.lerp)

  1. <details> 0fr→1frClipRect(AnimatedSize(200ms, easeInOut)) — the app's own accordion_block idiom.
  2. 3-bar <rect> svg → 3 rounded Containers in a Column (widths ~10/10/7) — pure widgets, no SVG asset, no CustomPaint (honours "Bootstrap Icons, not SVGs").
  3. video play / audio waveform drawn svgsBootstrapIcons.play_fill / BootstrapIcons.soundwave (the doc glyph is already a bi- class) — the app's mandated icon set; nearest-equivalent, not a pixel copy. These are the only non-literal mappings; everything else is an exact value port from kon_guide_v2.css.

Consumers & call-site impact (so 22.3 has no surprises)

  • GuideThumbnailStripGuideGalleryDisclosure (9 sites, same collection: arg, drop thumbSize:): material_row.dart:145, flashcard_panel.dart:129, step_block.dart:144,252, step_collection_block.dart:489,704, guide_lightbox_step.dart:143, guide_lightbox_lesson_page.dart:236, guide_lightbox_review.dart:116. (The lightbox sites correctly get the same shared disclosure — it's o_mat_gallery everywhere on the web.)
  • TimelineCircleStepDisc: step_collection_block.dart (ordinal). Standalone step_block.dart gains a StepDisc + drops StepBadgeRow type badge — in 22.3 (layout).
  • Neutralized labels / de-iconed atoms render immediately wherever used (material_row, step_block, collection blocks) — that's the visible 22.2 change; layout stays until 22.3.

Files touched (22.2)

  • Restyle: timeline_circle.dartStepDisc, material_type_badge.dart, step_badge.dart, tip_box.dart, explanation_box.dart.
  • New: GuideGalleryDisclosure, MaterialThumb, GuideSection (faithful ports of o_mat_gallery/o_mat_thumb/ .g-acc).
  • Update call sites: the 9 strip sites (swap widget). material_row thumb/inline-player + the disc additions are 22.3 (layout) — not 22.2.
  • Verify-only (no edit): content_block_renderer.dart / block_wrapper.dart (§5C).
  • Tokens: read 22.1 g-* only; brand via colorScheme.primary. Old guide* consts stay (removed 22.6).

Resolved decisions (formerly "open" — now closed from source)

  • D22.2-1 disclosure mechanismaccordion_block.dart's AnimatedSize pattern (not ExpansionTile). ✓ source.
  • D22.2-2 3-bar glyph → 3 rounded Containers (drawn widgets, no SVG/CustomPaint). ✓ matches the 3 <rect>s.
  • D22.2-3 question sub-type → neutral .g-qtype gFaint lowercase label (CSS:139, _block_step.gohtml:35); step-type badge removed. ✓ source.
  • D22.2-4 GuideSection.g-acc used only by material-collection (_block_material_collection.gohtml:23); build via the accordion pattern, one consumer. ✓ source.
  • D22.2-5 atom API → strip swap = same collection: (drop thumbSize:); StepDisc gains content/state enums (call sites listed). ✓ enumerated.
  • 22.2-G §5C → already implemented (content_block_renderer + BlockWrapper); verify-only. ✓ source.

Depends-on

22.1 tokens (committed ddd2bb7b). Nothing else — data/models exist (Phase-5 build).

Acceptance / smoke (per master plan's per-sub-phase gate)

  1. Per-component source re-read → port → independent code-vs-kon_guide_v2.css audit (as 22.1).
  2. flutter analyze clean (0 errors/warnings; existing guide widget tests green).
  3. Screenshot review of each restyled atom in a real consumer vs the kontent web render → operator sign-off.
  4. On-device smoke (physical device): disclosure collapse/expand + tiles route by media_type; disc shows the right glyph per type + done/wrong; labels neutral; tip/explanation de-iconed; the de-rainbow reads correctly.

Flutter CLAUDE.md compliance (now checkable — not aspirational)

  • [x] White-label: disc/section/explanation brand surfaces read colorScheme.primary; semantics (gOk/gBad, warn trio) fixed; rainbow tokens leave these widgets' code paths. No hardcoded brand.
  • [x] Bootstrap Icons onlyplay_fill/soundwave/doc/chevron_down/check; the 3-bar glyph is drawn Containers, not SVG. No flutter_svg.
  • [x] Design tokens, not magic numbers — every value is a 22.1 DesignTokens.g* member or colorScheme.primary.
  • [x] Atomic naming — atoms (StepDisc, neutral labels) stay atoms; molecules (GuideGalleryDisclosure, MaterialThumb, GuideSection) stay molecules.
  • [x] No new architecture — reuses accordion_block's AnimatedSize + media_block's switch(mediaType); no ThemeExtension/provider; Color.lerp only via 22.1 helpers.

As-built (Step B — implemented as a spike to surface every element-level detail)

Per the operator's A→B→C→D plan: B finishes the implementation to discover every value/decision the component-level plan above left open; C reverts this code and validates THIS section; D rebuilds from it. Every value below was read from source before it was written (rule A). Nothing here was guessed — items that are genuine product/structure choices are flagged DECISION (validate in C), not silently resolved.

Lesson (why B exists)

The plan above was component-level ("port o_mat_gallery"), not element-level (every sub-element, every value, every type-case, with file:line). That gap pushed real decisions — the .g-tile-tag duration label, the .g-tile-play scrim/shadow, the no-poster behaviour, the duration formatter — into coding. The plan must be element-complete before code. This section is that completion.

Files as-built (supersedes "Files touched" above)

  • New atoms: step_disc.dart (StepDisc), guide_media_tile.dart (shared tile primitives — see DEC-2).
  • New molecules: guide_gallery_disclosure.dart (GuideGalleryDisclosure), material_thumb.dart (MaterialThumb), guide_section.dart (GuideSection).
  • Restyled atoms: tip_box.dart, explanation_box.dart, material_type_badge.dart, step_badge.dart.
  • 3 additive tokens in design_tokens.dart: gTileIcGlyph=22 (.g-tile-ic :236), gTileLabelBg=Color(0x9E000000) (rgba(0,0,0,.62) .g-tile-tag/.g-mat-gal :239,80), gAccSumLetterSpacing=0.5 (~.04em@13 :222).
  • 9 call sites swapped GuideThumbnailStripGuideGalleryDisclosure (drop thumbSize:/gap:): material_row:145, flashcard_panel:129, guide_lightbox_lesson_page:236, guide_lightbox_review:116, guide_lightbox_step:143, step_collection_block:489,704, step_block:144,252. guide_thumbnail_strip.dart deleted. flutter analyze: 0 issues in all touched files (23 repo-wide infos are pre-existing, none mine).

Element-level resolution (pinned from source — this is what D builds)

Gallery tile .g-tile (_mat_gallery.gohtml, kon_guide_v2.css:234-239) — box paints gTint+overflow:hidden; dispatch by media.mediaType (== the exact strings media_block.dart:27 switches on): - imagepublicUrl cover (app pattern, guide_thumbnail_strip used publicUrl — DEC-7), fallback faint glyph. - videoalways .g-tile-play (poster optional): Stack[ poster img?, GuideTilePlay(play_fill), tag? ]. - audio → poster? Stack[ img, GuideTilePlay(soundwave), tag? ] : Stack[ GuideTileGlyph(soundwave,22), tag? ]. - document/other → GuideTileGlyph(file_earmark, 22) (.g-tile-ic). - .g-tile-play (:237-238) = white glyph (gThumbGlyph=20) over RadialGradient(radius:0.39 = 78% closest-side, [0x66000000→0x00000000]) + Icon.shadows drop-shadow Shadow(0,1,blur2,0x8C000000) (Icon.shadows verified in SDK icon.dart:85/216, Flutter 3.42 — DEC-3). - .g-tile-tag (:239) duration: bottom-left, 8px/w700/.24ls/white on gTileLabelBg, pad 1×4, radius 3 — DEC-1.

Material thumb .g-mat-thumb (_mat_thumb.gohtml, :75-80) — 48px (gThumb) box, gTint, gRadius; differs from the tile: no-poster video/audio = bare faint glyph, NO scrim (.g-mat-thumb svg=20, gThumbGlyph); doc = .g-tile-ic 22; null media = .g-mat-thumb--ph placeholder = image glyph #cfcfcf@20 (DEC-6). .g-mat-gal count badge (:80) bottom-right, 9px white on gTileLabelBg, pad 2×4, radius 3, shown when galleryCount>0. Doc icon = generic file_earmark (web uses mime-specific docIconClassDEC-8, deferred).

Section .g-acc (:220-226, _block_material_collection.gohtml:23) — open by default; bar .g-acc-sum = gTint/gRadius, pad gAccSumPadV/H (10/11), 13px/w700/uppercase/0.5ls/gInk, space-between; trailing (open)/ +(closed) in gBrandInk(primary)@gAccIndicatorSize(18) (the ::after); body .g-acc-body pad 4/0/11. Built on the accordion_block StatefulWidget+ClipRect(AnimatedSize 200ms) idiom.

Duration text — verbatim port of web formatDuration (internal/http/web/kontent/v1/handlers/module.go:284-295): <00:00; h>0h:mm:ss; else m:ss. (App had no shared formatter; timestamps.dart did m:ss only — DEC-5.)

DECISIONS to validate in C (logged, not silently chosen)

  • DEC-1 — .g-tile-tag duration label: web renders it on video/audio tiles; the 22.2-A bullet omitted it; the pre-added gTileTagFontSize token + "faithful port of o_mat_gallery" → included. Validate: keep or drop.
  • DEC-2 — shared guide_media_tile.dart atom (4th new file): .g-tile-play/.g-tile-ic/tag/count/formatter are the SAME CSS classes used by tile + thumb; one home prevents drift. Validate: accept the extra file vs inline-dup.
  • DEC-3 — drop-shadow via Icon.shadows (SDK-verified) rather than a codepoint hack. Validate: accept.
  • DEC-4 — no-poster behaviour differs by type (video always play overlay; audio/doc → faint glyph). Faithful to the two templates (_mat_gallery vs _mat_thumb differ). Validate: confirm intended.
  • DEC-5 — duration formatter ported from Go verbatim into guide_media_tile.dart. Validate: accept vs reuse.
  • DEC-6 — placeholder glyph #cfcfcf (CSS :77) rendered as an explicit color, not gFaint. Validate: token it?
  • DEC-7 — image/poster source: image=publicUrl, poster=edges.thumbnail.publicUrl (app pattern; web uses the thumbnail variant for images). Validate: keep app pattern vs resolve the variant.
  • DEC-8 — doc icon = generic file_earmark (web docIconClass is mime-specific). Validate: port the map later?
  • DEC-9 — material_row.galleryThumbSize now unused after the swap (public field, not flagged); left for the 22.3 material_row layout pass. Validate: confirm 22.3 removes it.

Verification done in B

  • flutter analyze clean (touched files: 0 issues).
  • flutter test (full suite): +277 −16. All 16 failures are in the guide UI test files only (guide_atoms_test 10, guide_molecules_test 2, guide_organisms_test 4) — zero failures outside the guide scope (auth/posts/reactions/notifications/content_block_renderer/series all green). Every failing assertion's Actual is Found 0 widgets with text "INSTRUCTION"/"QUESTION"/"INGREDIENT"/"TOOL" or a removed icon (lightbulb/info_circle/clock) — i.e. every failure checks for a label/icon UTS deliberately removed; none shows missing real content (names, instruction text, answers all still found). → The widgets are correct per the new design; the tests are STALE (they encode the pre-UTS look).
  • Build + on-device smoke deferred to D (post-validation) per the operator's A→B→C→D plan.

LESSON-2 (the one that nearly hid) — analyze ≠ tests

Changing an atom's behaviour (de-icon / de-rainbow / drop a label) keeps its constructor unchanged, so flutter analyze stays green — but the design tests that assert the old look go red. A behavioural restyle MUST be gated on flutter test, not just flutter analyze. B's first "double-check" stopped at analyze and missed all 16; running the suite is the only thing that surfaced them. The changed atoms connect to the guide test suite (and nothing else in lib/).

DEC-10 — update the 16 stale guide tests to the new behaviour (D must do; acceptance #2 = "guide widget tests green")

Exact edits (assert what the new widgets actually render): - StepBadgeRow (guide_atoms_test + guide_organisms StepBlock): drop the INSTRUCTION/QUESTION type-badge asserts (disc carries the type now); qtype is lowercase (multiple choice / true / false / flashcard); no clock icon on duration (assert 15 min text only). - TipBox / ExplanationBox (guide_atoms_test): drop the lightbulb / info_circle icon asserts; keep the text. - MaterialTypeBadge / MaterialRow / MaterialCollectionBlock / MaterialItemBlock (guide_atoms + guide_molecules + guide_organisms): the type label is the lowercase raw materialType (.g-mat-type has no text-transform) — assert 'tool'/'ingredient', NOT 'TOOL'/'INGREDIENT'. - Leave the TimelineCircle + OptionalBadge tests as-is (TimelineCircle unchanged until 22.3; OptionalBadge text unchanged — both already green).

Live validation (B-spike plan — SUPERSEDED by the D rebuild; see "SHIPPED" at the end)

Goal: visually validate the B widgets on the running macOS app before C reverts, so D rebuilds knowing they render right. Environment (all live, survive a Claude Code restart): - macOS app running via detached flutter run -d macos (B code). Registered in marionette as gokonversed (marionette doctor = OK). Backend up at https://kon.test:8443 (dev, main site, [email protected] authed). - Use the marionette MCP (loads on restart: marionette_mcp 0.5.0 + .mcp.json + grants all present). Its swipe is the unlock — CLI scroll-to works only on built widgets (fine inside a post detail, fails on the lazy feed), so swipe is the only way to reach the gallery-bearing posts. Validated so far: app boots + renders with B code, white-label red main theme correct (app bar/hero/FABs), feed + Guides-category filter + post-detail hero all render, zero crashes. Still to validate (the visible B changes): 1. De-iconed / neutralized atoms in a guide post body (reachable via scroll-to since post-detail blocks are built): TipBox/ExplanationBox (no lightbulb/info icon), MaterialTypeBadge (lowercase tool/ingredient, not a colored UPPER badge), StepBadgeRow (no INSTRUCTION/QUESTION type badge, lowercase qtype, no clock on duration). 2. GuideGalleryDisclosure (the big structural change) — needs a step/material with a media gallery. Targets: the-art-of-sourdough-bread (post 46) and introduction-to-hand-tool-woodworking (49) — both have gallery- bearing steps+materials in the post body AND the attached guide lightbox. Reach via swipe down the Guides- category feed (lazy list) → open post → swipe to the step/material block → tap "Show N" → confirm the disclosure expands a horizontal strip of 64px type-aware tiles (image cover / video+play overlay / etc.). 3. Guide lightbox (tap-navigable Next/Prev) on 46/48: confirm the same atoms render inside the lightbox steps. Mixology (48) has the atoms but NO galleries; sourdough (46) is the richest single target.

Live validation — DONE (marionette macOS, post 46 sourdough, red main brand)

All 7 visible B widget groups validated on the running app via get_interactive_elements (exact colors/sizes read off the live render), screen 433×968 logical. Flutter brand = back_fab border rgb(207,0,21) = #cf0015. 1. TipBox — text #7A5B00 (gWarnInk), 13px, no icon (2 instances: "Pause the video…", "Always check your image alt text…"). 2. GuideGalleryDisclosure — "Show 2"/"Show 8" summary #810F1B (gBrandInk of red brand), 11px, w600; expands a strip of 64px tiles at 6px gaps; expand/collapse chevron works. 3. MaterialTypeBadge — lowercase ingredient/supply/software, #9AA0A6 (gFaint), 11px. 4. OptionalBadge — "OPTIONAL" bordered pill, #9AA0A6, 10px, letter-spacing 0.3. 5. StepBadgeRow duration — "15 min" #6B7280 (gMuted), 10px, no clock, light pill bg; the step title has no INSTRUCTION type badge. 6. StepBadgeRow qtype — "true / false"/"flashcard" lowercase, #9AA0A6 (gFaint), 11px, no icon, no QUESTION badge. 7. ExplanationBox — triggered by answering the TF question (select False → Submit): "True! Earth orbits the Sun…" text #3A3A3A (gBody), 13px, no icon, brand left border + tint bg. Answer feedback also correct (True=green #0F5132, False=red #842029). State reset via "Try again". Deferred-and-correct: material qty still renders blue #1565C0 (old guideMaterialQuantityColor) — repoint to gInk is a 22.3 item, not a B regression.

Web cross-validation — DONE (playwright-cli uts-smoke, same dev post 46, kon_guide_v2.css)

Read the authoritative web computed styles (brand --kon-brand-color = #cf0015, identical to Flutter) and compared element-for-element. Verdict: 7/8 exact matches, 1 gap.

Element Web (kon_guide_v2.css) Flutter (B) Match
brand #cf0015 back_fab rgb(207,0,21)
.g-tip #7a5b00 13px, bd-left 3px #f0c000, bg #fff8e1, no icon #7A5B00 13px, no icon
.g-qtype #9aa0a6 11px, no icon #9AA0A6 11px, no icon
.g-step-dur #6b7280 10px, bg #f7f7f8, no icon #6B7280 10px, pill bg, no icon
.g-explain #3a3a3a 13px, bd-left 3px #cf0015, bg #f7f7f8, no icon #3A3A3A 13px, brand bd-left, no icon
.g-mat-opt #9aa0a6 10px, ls 0.3px, uppercase, bd 1px #e2e2e2 #9AA0A6 10px, ls 0.3, bordered
.g-gal summary "Show N" dark-red gBrandInk + down chevron (screenshot) #810F1B 11px w600 + chevron
.g-mat-type text "· ingredient" (middot prefix), #9aa0a6 11px text "ingredient" — NO middot, #9AA0A6 11px ⚠️ GAP

GAP → D-FIX (in-scope for the de-rainbow atom, 22.2-E): web prefixes the material type label with a "· " middot separator ("Bread Flour · ingredient", "Dutch Oven · tool"); Flutter's MaterialTypeBadge renders only "ingredient". D: prepend "· " in material_type_badge.dart (or render it as a leading separator span) so name·type reads like web. (Full material_row layout adoption stays 22.3; only the atom's text needs the middot.)

Confirmed-deferred (22.3, expected not-adopted — web shows them, Flutter doesn't yet): - Material accordion groups (g-acc "Dough −"/"Equipment −") → GuideSection (created, not wired). - Material thumbnail + count badge (g-mat-gal "8") → MaterialThumb (created, not wired). - Material qty color dark on web vs Flutter blue → repoint to gInk (22.3).

Data-not-render (ignore): "500g g" unit doubling = the known dev-seed-only issue ([[project_material_unit_doubling_dev_seed_only]]); prod data is clean.

Method note: read computed styles with eval(getComputedStyle) for static elements only; used snapshot + element screenshot (not eval) for anything touching nav/disclosure state, per operator guidance that eval is unreliable across the nav/button sequence.


SHIPPED — phase-D clean rebuild (2026-06-29) — committed ec30c714 (push-pending GoLand)

The D rebuild was authored straight from this doc (not the B-spike code) and reproduced the spike set as a superset; the recovery-net stash uts-phase22-B-spike was dropped once the commit superseded it.

Commit ec30c714 (master) — 21 files, +1053/−526 (15 M, 5 new, 1 del). EXCLUDED the local site_config.dart (siteSlug=main dev switch) + .claude/settings.local.json. Contents = the 5 new widgets + 4 restyled atoms + ~50 tokens + the 9 gallery-disclosure call-site swaps + guide_thumbnail_strip.dart deletion + the 16 DEC-10 test updates.

Pre-commit verification pass (operator-requested). Diffed every widget/token against kon_guide_v2.css (extracted independently by a 2nd Explore agent) + the real template SVG — nothing assumed: - StepDisc 3-bar glyph = EXACT vs _block_step.gohtml:9 <svg viewBox="0 0 16 16"> rects 10/10/7 × h1.6 @ y-pitch 3, rx.8 → ×0.9375 → tokens 9.4/9.4/6.6 × 1.5, gap 1.3, fully-rounded, left-aligned. ✅ - gBrandInk Color.lerp(brand, ink, .45)color-mix(in srgb, brand 55%, ink). ✅ - GuideSection correctly models .g-acc/.g-acc-sum (the post-body _block_material_collection.gohtml:23 <details class="g-acc" open>, open-default, −/+). The lightbox's flat .g-grouphdr is a separate widget → 22.4, not GuideSection. ✅ - gAccMarginBottom=4.g-acc{ margin-bottom:4px } (CSS:221). ✅ · all other component tokens match (sub-pixel px→dp rounding only). - 1 real bug found + fixed: material_thumb doc/default glyph rendered at the 64dp-tile default 22dp but .g-mat-thumb svg{ width:20px } → fixed to gThumbGlyphSize (20). Latent (mat-thumb not adopted until 22.3). - Minor, deferred by design (no fix): disclosure/section anims 200ms (the app's accordion_block idiom, per the Patterns table) vs CSS .25s/.28s; .g-strip reveal padding-top .4rem omitted; one SizedBox(width: 8) literal (≡ .5rem).

Live web↔Flutter A/B re-verified on the D rebuild (marionette MCP on a freshly-rebuilt macOS app + playwright-cli uts-smoke headed, post "The Art of Sourdough Bread", red #cf0015 main brand): - Step block + both material surfaces (post-body + lightbox cook) match web. The only visible delta = blue step discs (old TimelineCircle) vs web's red brand disc = the un-adopted StepDisc → 22.3, NOT a regression. Run log: 0 exceptions / overflow. - Lightbox A/B confirmed the same scope boundary: middot + "Show N" match; the masthead colored pills, metadata icons, PT30M/1440 min raw duration, and blue DOUGH box are the old pre-22.4 lightbox shell → 22.4/22.5. Flagged for 22.4: humanize the lightbox metadata duration (web shows "30 min" / "24 hrs").

Gate (per master per-sub-phase rule): 1 source-audit ✅ · 2 flutter test 293 green + analyze clean (23 pre-existing share_plus only) ✅ · 3 screenshot review vs web ✅ operator signed off 2026-06-29 · 4 on-device smoke = the operator's physical-phone step (pending).

NEXT → 22.3: adopt these atoms into the 4 post-body blocks (TimelineCircleStepDisc turns the discs brand-red; material qty blue→ink; wire MaterialThumb + GuideSection).

← Back to Under the Tuscan Sun