Phase 22.3 — Furnishing the Rooms
05 JUL AT 08:37 PM

Phase 22.3 — Furnishing the Rooms

0 LOVES 0 VIEWS
The four post-body blocks.

UTS Phase 22.3 — Flutter post-body blocks (adopt the 22.2 atoms into the 4 content blocks)

The adoption layer. 22.1 laid the tokens; 22.2 built + restyled the shared atoms; 22.3 wires them into the four post-body guide content blocks so the post body reaches web parity. Element-complete: every edit below is pinned to a file:line in TWO grounded sources — nothing is "verify at build", nothing is guessed. (This is the bar that let 22.2's rebuild go one-pass; the 22.2 spike needed a redo precisely because its doc wasn't yet here.) - Look/values + target markupstatic/css/kon_guide_v2.css + the kontent templates (_block_*, _mat_thumb, block-interaction-compiler.js). The web is the reference. - What's there + the app idiom ← the existing Flutter blocks (lib/content_blocks/widgets/…) + the 22.2 atoms. Where source and any summary differ, the source wins. Brand via colorScheme.primary; values via g-* tokens.

Program: Under the Tuscan Sun → Phase 22 → builds on 22.1 tokens + 22.2 shared components. Owner: Flutter (with Travis). Tooling: Android Studio (device + hot-reload + on-device smoke).

Goal

Make the four post-body guide blocks render at web parity by adopting the 22.2 atoms, moving qty to the trailing edge, and repointing the last rainbow colors to neutral: 1. StepCollectionBlockTimelineCircleStepDisc (brand ordinal disc, no halo; done→✓ / wrong→✗). 2. StepBlock — gain a StepDisc type glyph (3-bar instruction / ? question) it currently lacks; .g-step row. 3. MaterialRow — restructure to .g-mat ([thumb][body flex:1][qty-trailing]): qty blue→ink and moved to the trailing edge; inline image thumb → type-aware MaterialThumb; row separator (border-bottom, not striping). 4. MaterialCollectionBlock — static blue group headers → collapsible GuideSection ("Materials" fallback). Plus: verify §5C chrome (.g-sectiontitle / .g-md-v / .g-empty / .g-progress) and retire TimelineCircle.

Scope

In: the 4 blocks + MaterialRow adopt the 22.2 atoms; the qty move; the row separator; the group expander; repoint material-qty + group colors to neutral; retire the TimelineCircle widget; add the new g-* metric tokens. Out: - The lightbox (shell/cook/learn/review/picker) — separate surface → 22.4 / 22.5 (still uses old rainbow). - The nonsequential learn-flow behavior22.5 (D22.1). - Deleting the old DesignTokens.guide* consts22.6 (still read by the lightbox). 22.3 stops using the post-body ones; 22.6 removes them.

Patterns this phase follows (compose, don't reinvent) — grounded

Need The atom/idiom to use (cited) The web rule it satisfies
Step disc (ordinal / type glyph / done / wrong) StepDisc (step_disc.dart:32-37) .g-step-num (kon_guide_v2.css:91-93; _block_step.gohtml:9,29; _block_step_collection.gohtml:39)
Per-material type-aware thumb + count MaterialThumb (material_thumb.dart:22-27) _mat_thumb.gohtml + .g-mat-gal
Collapsible material group (open-default, −/+) GuideSection (guide_section.dart:14) <details class="g-acc" open> (_block_material_collection.gohtml:23)
Neutral qty / labels gInk, gFaint, gMuted .g-mat-qty neutral ink (kon_guide_v2.css:86)
Block chrome block_wrapper.dart:5-48 + content_block_renderer.dart:49-102 .g-sectiontitle/.g-md-v (:48,289)

Element-level resolution (pinned — this is literally what the builder builds)

22.3-A — StepCollectionBlock: TimelineCircleStepDisc (2 sites)

Sites: read-only step_collection_block.dart:209, interactive :395 — both sit in Padding(bottom:16, child: Row(crossAxisAlignment: start)[ <circle>, SizedBox(width:12), Expanded(<body>) ]). Replace TimelineCircle(number: n, circleState: cs)StepDisc(content: StepDiscContent.number, number: n, state: <map(cs)>). Keep the Row/SizedBox(12)/Expanded. State map (port the existing derivation verbatim): - read-only (:200-202): cs = stepType=='instruction' ? instruction : question → both → StepDiscState.normal (a collection shows the ordinal; the type no longer colors the disc — de-rainbow). - interactive (:382-388): cs = hasInteraction ? (isIncorrect ? answeredIncorrect : completed) : <type>, where isIncorrect = isSubmitted && correctOptionId != null && selectedOptionId != correctOptionId (:378-380). → completed → StepDiscState.done (✓); answeredIncorrect → StepDiscState.wrong (✗); else normal. Web confirms (block-interaction-compiler.js): completed or answered-correct → ✓ ✓ (--completed/--answered, g-ok); answered-incorrect → ✗ ✗ (--answered-incorrect, g-bad); pending → ordinal. StepDisc already renders done→gOk+✓ / wrong→gBad+✗ (step_disc.dart:41-57) — exact match. No halo; 24dp→30dp.

22.3-B — StepBlock: add a StepDisc type glyph + .g-step row (the HIGHEST-touch edit — 4 paths)

Web .g-step (kon_guide_v2.css:89): standalone step = a flex row [.g-step-num disc][body]; instruction → 3-bar glyph (_block_step.gohtml:9), question/flashcard → ? (:29). Current Flutter — StepBlock has FOUR head-rendering paths, each a Column[ StepBadgeRow, title?, … ], NONE with a disc (read myself, not a subagent): - read-only non-flashcard — step_block.dart:208 - read-only flashcard — :175 (body = FlashcardPanel(interactive:false)) - interactive non-flashcard — :99 - interactive flashcard — :284 (body = FlashcardPanel(interactive:true)) Target (each of the 4): wrap the head Column in Row(crossAxisAlignment: start)[ StepDisc(content:<glyph>, state:<map>), SizedBox(width: gStepGap≈13.6), Expanded(<the Column>) ]. A private _stepHead(...) helper returning that Row avoids 4× duplication (the 4 Columns share the StepBadgeRow+title prefix verbatim). - content: stepType=='instruction' → StepDiscContent.bars; 'question' (incl. flashcard) → StepDiscContent.question. - stateCORRECTED: StepBlock does NOT use interaction.isCorrect; it derives correctness itself (verified step_block.dart:80-86, same pattern as the collection): - read-only paths (:175,:208): always StepDiscState.normal (no interaction). - interactive non-flashcard (:99, state at :69-86): isChecked (interactionType=='complete') → done; else if isSubmitted (interactionType=='answer') → (interaction.selectedOptionId == correctOptionId) ? done : wrong, where correctOptionId = correctOption?.id ?? step.edges.answerOptions.where((o)=>o.isCorrect).firstOrNull?.id (:80-86); else normal. - interactive flashcard (:284): revealed (== hasInteraction, :94) → done; else normal. (Web: .o-block-step--completed .g-step-num::after = ✓ — done shows ✓.)

22.3-C — MaterialRow: restructure to .g-mat (the biggest edit)

Web .g-mat (kon_guide_v2.css:73): display:flex; align-items:center; gap:.75rem(12); padding:.55rem 0(8.8); border-bottom:1px var(--g-line). Children = [thumb][.g-mat-body flex:1][.g-mat-qty trailing]. Current Flutter (material_row.dart:29-151): Column[ Row(start)[ if hasImage: ClipRRect(image thumb)+gap, Expanded(Column[ Wrap[ qty(blue,leading) + unit + name + MaterialTypeBadge + OptionalBadge ], note ]) ], non-image media block, GuideGalleryDisclosure ]. → Restructure to (web _block_material.gohtml:8-21 exactly — .g-mat row, then o_mat_gallery as a SIBLING):

Column( crossAxisAlignment: start, children: [
  Container( // .g-mat row (_block_material.gohtml:8-16)
    decoration: border-bottom 1px gLine,       // replaces the alt-row striping (DEC-22.3.7)
    padding: EdgeInsets.symmetric(vertical: gMatRowPadV≈8.8),
    child: Row( crossAxisAlignment: center,     // .g-mat align-items:center (was start) (DEC-22.3.6b)
      children: [
        MaterialThumb(media: <primaryMedia>, galleryCount: <galleryCount>, onTap: <openPrimaryMedia>),  // 48dp
        SizedBox(width: gMatRowGap≈12),
        Expanded( child: Column( crossAxisAlignment: start, children: [   // .g-mat-body flex:1
          Wrap(spacing:4, runSpacing:4, crossAxisAlignment: center, children: [
            Text(name, gMatNameFontSize=15 / gInk),
            MaterialTypeBadge(materialType), if(optional) OptionalBadge(),
          ]),
          if(notes) Padding(top: gMatNoteMarginTop=3, child: Text(notes, 12 / gFaint / italic)),
        ])),
        if(qty|unit) Padding(left: gMatQtyPadLeft≈6.4,                   // .g-mat-qty TRAILING (DEC-22.3.6)
          child: Text('${qty}${unit==''?'':' $unit'}', gMatQtyFontSize=14 / w600 / gInk)),
      ])),
  if (hasGallery) GuideGalleryDisclosure(collection: material.edges!.mediaGallery!),  // o_mat_gallery — KEPT (:21)
])

Key changes vs today: (1) qty moves leading→trailing + blue→gInk, 14/600 (:86); (2) inline image → MaterialThumb (type-aware + count badge); (3) KEEP GuideGalleryDisclosure as a row sibling — (CORRECTED: the web does NOT drop it; o_mat_gallery renders after the .g-mat row in BOTH _block_material.gohtml:21 and _block_material_collection.gohtml:39). The thumb opens the primary media (o_mat_thumb comment + data-mc-item on $pm), the disclosure shows the separate MediaGallery; the .g-mat-gal badge = gallery count. (4) drop the below-row non-image inline media player (DEC-22.3.8 — web has none: "no inline player (Y)" _block_material.gohtml:18; primary video/audio is reachable via the thumb tap); (5) border-bottom separator replaces alt striping (.g-mat :73; web drops it on :last-child :74 — Flutter accepts the minor delta of a trailing hairline, or the collection caller can strip the last row's border); (6) merge the separate qty + unit Texts (:74-94) into one trailing .g-mat-qty ('${qty}${unit==''?'':' $unit'}'); (7) MaterialRow now owns the .g-mat Container (border-bottom + vertical padding), so its 4 params go DEADthumbSize, thumbRadius, galleryThumbSize, mediaMaxWidth (:14-17, fixed 48dp/gRadius now) — remove them from the constructor (cascade in 22.3-E). hasImage/ hasNonImageMedia branching (:30-32) collapses to hasGallery only — MaterialThumb takes material.edges?.media of any type, and the inline media path is gone. Wiring the thumb onTap (open the PRIMARY media — Media→single-item lightbox, by type; both factories CONFIRMED):

onTap: media == null ? null : () {
  if (media.mediaType == 'image')
    openGalleryLightbox(context, images: [GalleryImage.fromMedia(media)], initialIndex: 0);  // gallery_image.dart:38
  else if (media.mediaType == 'video' || media.mediaType == 'audio')
    openMediaLightbox(context, tracks: [Track.fromMedia(media)], initialIndex: 0);           // track.dart:168
  // doc/other → web opens a new tab; Flutter no-op for now (FLAG: optional url_launcher, out of 22.3 scope)
}

(NOTE — this is NOT the disclosure's routing: GuideGalleryDisclosure._openLightbox (:201-208) opens the gallery items via openGalleryLightbox/GalleryImage.fromCollectionItem. The thumb opens the primary Media instead.) - MaterialThumb.media = material.edges?.media (the primary Media, confirmed guide_material.dart:34). - galleryCount = material.edges?.mediaGallery?.edges?.items?.length ?? 0 (confirmed media_collection.dart:22,32). - GalleryImage.fromMedia(Media) (confirmed gallery_image.dart:38); Track.fromMedia(Media,{index}) (confirmed track.dart:168); openGalleryLightbox(context, images:, initialIndex:) (gallery_lightbox.dart:20); openMediaLightbox(context, tracks:, initialIndex:) (media_lightbox.dart:30). - Imports for material_row.dart: add '../atoms/material_thumb.dart', '../../../../shared/widgets/organisms/gallery_lightbox/gallery_image.dart', '../../../../shared/widgets/organisms/gallery_lightbox/gallery_lightbox.dart', '../../../../shared/widgets/organisms/media_lightbox/media_lightbox.dart', '../../../media/data/models/track.dart'. Drop '../../../../content_blocks/widgets/media_block.dart' (:6, inline player gone) and 'network_image_or_svg.dart' (:4, if unused after the inline image is removed — verify). Keep 'guide_gallery_disclosure.dart' (:8) — gallery stays.

22.3-D — MaterialCollectionBlock: static blue headers → GuideSection, drop the outer card

Current (material_collection_block.dart): Column[ title(Container+border-bottom, :26-45), desc(:49-60), Container(border+radius+clip — the CARD, :62-74)[ Column(_buildGroupedRows) ] ]. _buildGroupedRows (:79-103) is a flat loop emitting _buildGroupHeader (blue guideMaterialGroupBg #E3F2FD / guideMaterialGroupColor #1565C0, :105-128) on group-change, then _buildMaterialRow(material, isAlt) (:130-157, a Container with isAlt bg + left border + bottom border wrapping MaterialRow) per item; ungrouped (group==null) gets NO header. Web target (_block_material_collection.gohtml): <div class="o-block-material-collection mb-4">[ g-sectiontitle, g-md-v, per-group <details class="g-acc" open> ]NO bordered outer card (.g-acc is just a tint bar, kon_guide_v2.css:221-226); each group = one open .g-acc; ungrouped/null → a single "Materials" .g-acc (:23). So: 1. Delete the outer card Container (:62-74, border+radius+clip) → emit the GuideSections straight into the parent Column. (.g-acc supplies the only chrome — DEC-22.3.3b parity.) 2. Delete _buildGroupHeader (:105-128) and _buildMaterialRow (:130-157) entirely — GuideSection owns the header (its .g-acc-sum tint bar) and MaterialRow now owns the .g-mat row Container (22.3-C). The blue header, isAlt striping, and the per-row left-border all go away (de-rainbow + parity). 3. Refactor _buildGroupedRows to accumulate rows per group, then wrap each group once:

final groups = <String, List<Widget>>{};  final order = <String>[];
for (final item in items) {
  final key = (item.group?.isEmpty ?? true) ? 'Materials' : item.group!;  // null OR "" → "Materials" (web parity, :19/:23)
  groups[key] ??= (order..add(key), <Widget>[]).$2;   // preserve first-seen order
  final m = item.edges?.material; if (m != null) groups[key]!.add(MaterialRow(material: m));  // no params (22.3-C)
}
return order.map((k) => GuideSection(title: k, child: Column(children: groups[k]!))).toList();

(Forward-pass first-seen order; GuideSection open-by-default so all rows visible. The web .g-mat:last-child drops its border-bottom inside each group — Flutter accepts the trailing hairline, or strip groups[k].last's border. FLAG.) Stop reading guideMaterialGroup{Bg,Color,…}, guideMaterialRow{AltBg,BorderLeft,…}, guideMaterialCardBorderRadius (physical deletion → 22.6). Title/desc chrome (:26-60) is verified separately in 22.3-F.

22.3-E — MaterialItemBlock: drop the dead params (caller cascade from 22.3-C)

(CORRECTED — NOT "no own work".) material_item_block.dart:19-24 currently passes three params to MaterialRow: thumbSize: guideMaterialThumbSize, thumbRadius: guideMaterialThumbRadius, galleryThumbSize: guideStandaloneGalleryThumbSize. Since 22.3-C removes those params, this call must become MaterialRow(material: material) (and its now-unused design_tokens import dropped if nothing else uses it — verify). Behavior to confirm after: single material renders thumb + qty-trailing-ink + the GuideGalleryDisclosure sibling when a gallery exists.

22.3-F — §5C block chrome (exact values, verify + align)

Element Web class (kon_guide_v2.css) Exact values Flutter site
Collection title .g-sectiontitle (:48) 18.4px (1.15rem) / w500 / gInk, margin 25.6/0/8 step_collection_block.dart:77-111, material_collection_block.dart:26-60
Collection desc .g-md-v (:289) 13.5px / gBody same
Empty state .g-empty (:293) pad 20, 1px dashed gLine2, gRadius, 13px / gFaint / center collection empty branch
Progress bar .g-progress/-fill (:184-186) track h4 / gLine2 / r2, fill gOk (or --brand=brand), margin 4/0/16 step_collection_block.dart progress
Step item sep .o-block-step-collection-item (:197) border-bottom 1px gLine, :last-child none collection item wrapper
Heading/divider/hidden stay owned by BlockWrapper/content_block_renderer (verified correct in 22.2-G).

22.3-G — retire TimelineCircle

After A, TimelineCircle has zero call sitesdelete timeline_circle.dart + its test group (guide_atoms_test.dart:108-127). Keep the guide* color/halo token consts until 22.6.


New tokens to add (22.3) — additive, g-*, from kon_guide_v2.css

Insert after design_tokens.dart:654 (end of the 22.2 guide-component token block, before the "Phase 5a" section).

gSectionTitleFontSize = 18.4   // .g-sectiontitle 1.15rem (:48)
gSectionTitleWeight   = w500
gSectionTitleMarginTop= 25.6   // 1.6rem
gSectionTitleMarginBot= 8      // .5rem
gMdValueFontSize      = 13.5   // .g-md-v (:289)
gMatRowGap            = 12     // .g-mat gap .75rem (:73)
gMatRowPadV           = 8.8    // .g-mat padding .55rem 0
gMatNameFontSize      = 15     // .g-mat-name (:82)
gMatQtyFontSize       = 14     // .g-mat-qty (:86)
gMatQtyWeight         = w600
gMatQtyPadLeft        = 6.4    // .4rem
gMatNoteFontSize      = 12     // .g-mat-note (:85, italic)
gMatNoteMarginTop     = 3
gEmptyPad             = 20     // .g-empty 1.25rem (:293)
gEmptyFontSize        = 13
gProgressHeight       = 4      // .g-progress (:184)
gProgressRadius       = 2
gStepGap              = 13.6   // .g-step gap .85rem (:89)
gStepHeadGap          = 8      // .g-step-head gap .5rem (:95)

Colors reuse 22.1: gInk #222, gBody #3a3a3a, gFaint, gLine, gLine2, gOk, gBad, gTint, gRadius, gDisc. Stop reading guideMaterialQuantityColor, guideMaterialGroup{Bg,Color}, the guideTimelineCircle* halos (physical deletion → 22.6).

CSS→Flutter translations

Web Flutter Note
disc content rewrite to / (JS) StepDisc state: done/wrong renders / the disc owns the mark
.g-mat flex [thumb][body][qty] Row[MaterialThumb, gap, Expanded(body), qty] qty trailing
<details class="g-acc" open> per group per-group GuideSection accumulate rows, then wrap; no outer card
o_mat_thumb data-mc-item opens PRIMARY media MaterialThumb(onTap:)GalleryImage.fromMedia / Track.fromMedia by type; single-item lightbox
o_mat_gallery "Show N" (sibling) GuideGalleryDisclosureKEPT the SEPARATE MediaGallery
.g-mat border-bottom (:last-child none) Container(border-bottom 1px gLine) replaces alt striping

Consumers & call-site impact

  • StepDiscstep_collection_block.dart:209,395 (swap) + step_block.dart ×4 head paths (:99,175,208,284, new _stepHead wrap).
  • MaterialThumbmaterial_row.dart:42-62 (replace inline image) + its onTapprimary media (not the gallery).
  • GuideSectionmaterial_collection_block.dart (delete outer card :62-74 + _buildGroupHeader :105-128 + _buildMaterialRow :130-157; refactor _buildGroupedRows :79-103).
  • GuideGalleryDisclosureKEPT in material_row.dart:142-148 (web renders it as a .g-mat sibling — CORRECTED).
  • below-row non-image inline media playerremoved from material_row (DEC-22.3.8; primary via thumb tap).
  • MaterialRow params ← removed → cascade edits at material_item_block.dart:19-24 (drop 3 params) + material_collection_block.dart _buildMaterialRow (deleted).
  • TimelineCircle ← deleted (widget + test group).

Files touched (22.3, projected)

lib/content_blocks/widgets/{step_block,step_collection_block,material_collection_block,material_item_block}.dart (all four — material_item_block + material_collection_block carry the MaterialRow-param cascade) · lib/features/guides/ui/molecules/material_row.dart · lib/config/design_tokens.dart (add the metric tokens above) · delete lib/features/guides/ui/atoms/timeline_circle.dart · tests (next section).

Test plan (the DEC-10 equivalent — flutter test green first try)

Delete (widget gone): - guide_atoms_test.dart:108-127 — the entire TimelineCircle group (enum obsolete). Add (the 3 atoms have ZERO coverage today — close the gap): - StepDisc group (guide_atoms_test): content {bars,question,number} × state {normal,done,wrong}; assert ✓ on done, ✗ on wrong, the ordinal on number, 30dp, no halo, white content. - GuideSection group (guide_molecules_test): renders uppercase title / gInk / gTint bar; tap toggles child visibility (AnimatedSize); (open)/+(closed) in gBrandInk. - MaterialThumb group (guide_molecules_test): 48dp; image/video+play/audio+waveform/doc glyph/--ph placeholder; .g-mat-gal count badge when galleryCount>0; onTap fires (opens the primary media, distinct from the gallery disclosure). GuideGalleryDisclosure already has coverage; the MaterialRow test must keep finding it (it stays). Keep (verify still pass — text finders survive the restyle): - guide_organisms_test.dart:507-508 (collection numbers 1/2 — now via StepDisc). - guide_organisms_test.dart:567-568 (DOUGH/EQUIPMENT — now via GuideSection). - guide_molecules_test.dart:175-190 (qty text 500g — color/position changed, text unasserted). - guide_organisms_test.dart:469-494 (MaterialItemBlock). No golden/visual tests exist — so the restyle has no golden baseline to update (flag: visual parity is caught by the screenshot-review gate, not CI).

Open decisions (logged — parity-resolved ones flagged, not silently changed)

  • DEC-22.3.1 (web-decided) — collection disc = ordinal; done→✓ / wrong→✗.
  • DEC-22.3.2 (web-grounded, visible ADD)StepBlock gains a type-glyph StepDisc (.g-step row).
  • DEC-22.3.3 (✅ RE-CONFIRMED — operator 2026-06-29, "yes, parity — keep the disclosure") — my first framing ("remove the disclosure; gallery-via-thumb") misread the web and was corrected. The web keeps BOTH surfaces per material: the tappable MaterialThumb opens the PRIMARY media (o_mat_thumb, data-mc-item on $pm), AND o_mat_gallery (the "Show N" GuideGalleryDisclosure) stays as a .g-mat sibling for the separate MediaGallery (_block_material.gohtml:21, _block_material_collection.gohtml:39). Resolved target = keep the disclosure; thumb opens the primary — full parity.
  • DEC-22.3.4 — delete the TimelineCircle widget + test in 22.3; defer guide* token consts to 22.6.
  • DEC-22.3.5StepDisc done/wrong from the existing BlockInteractionBloc state (port the mapping).
  • DEC-22.3.6 (✅ CONFIRMED — operator 2026-06-29) — material qty moves leading→trailing (.g-mat […][qty]).
  • DEC-22.3.7 (parity) — material row separator = border-bottom 1px gLine, drop the alt-row striping.
  • DEC-22.3.8 (✅ CONFIRMED — operator 2026-06-29)drop the below-row inline media block in material_row (web has none; the type-aware thumb + tap-to-open covers video/audio). Removes inline playback, by design.

Depends-on

22.1 tokens (ddd2bb7b) + 22.2 atoms (ec30c714: StepDisc, MaterialThumb, GuideSection, MaterialRow).

Acceptance / smoke (per master per-sub-phase gate)

  1. Per-component: re-open each block's cited source + the atom → adopt → independent code-vs-source audit.
  2. flutter analyze clean AND flutter test green (delete TimelineCircle group; add the 3 atom groups — analyze ≠ tests).
  3. Screenshot review vs the kontent web render (marionette MCP + playwright-cli uts-smoke) on "The Art of Sourdough Bread": step discs brand-red with ✓/✗; material qty ink + trailing; groups as GuideSection; type-aware thumbs + count; row border-bottom separators. → operator sign-off.
  4. On-device smoke (operator's physical-phone step).

Flutter CLAUDE.md compliance (checkable)

Atomic placement; design tokens not magic numbers (the metric tokens above); no ThemeExtension; Bootstrap Icons (the 3-bar disc = drawn Containers in StepDisc); white-label brand via colorScheme.primary; snake_case; no cross-module imports; freezed models untouched.

Confidence read (the 98%+ ask — honest, post-assumption-audit 2026-06-29)

An assumption audit (operator-requested — "check you have not guessed") re-read every edit site MYSELF and found the prior draft had relied on subagent summaries in places. Four real misses were corrected before this read: 1. Gallery/thumb semantics (the big one). Prior draft said "remove GuideGalleryDisclosure; gallery opens via the thumb." Wrong. Reading _block_material.gohtml:8-21 + _block_material_collection.gohtml:27-39 shows the web keeps both — the thumb opens the primary media; o_mat_gallery (the "Show N" disclosure) stays as a sibling for the separate gallery. Corrected in 22.3-C/D, Consumers, tests, DEC-22.3.3 (re-opened). Grounded the new thumb wiring on GalleryImage.fromMedia (gallery_image.dart:38) + Track.fromMedia (track.dart:168) — read, not assumed. 2. StepBlock correctness derivation. Prior draft implied interaction.isCorrect; the code (step_block.dart:80-86) derives it itself via selectedOptionId == correctOptionId. Corrected in 22.3-B; also enumerated all 4 head paths (:99,175,208,284), not 1. 3. Caller cascade. MaterialRow's 4 params go dead, so material_item_block.dart:19-24 (3 params) and material_collection_block._buildMaterialRow (:130-157, 4 params) must change — 22.3-E was wrongly "no own work." Corrected (22.3-E rewritten; both files added to Files-touched). 4. Outer card. The web material collection has no bordered card (.g-acc is a tint bar only); Flutter wraps in Container(border+radius+clip) (:62-74) → must be deleted. Added to 22.3-D. What IS solid: the disc state maps (verified vs the JS compiler + step_collection_block.dart:200-202,378-388), the qty-trailing/merge, the per-group refactor + empty-string fallback, the new tokens, the test delta, and every field/API now cited to a line I opened myself. Second (scoped) pass — clean. After the 4 corrections above, a follow-up audit targeted exactly the claims still riding on summary/subagent trust and re-read each from source: StepDisc API + ✓/✗/30dp/no-halo (step_disc.dart:6-107), GuideSection({required title, required child}) (guide_section.dart:14), .g-step gap .85rem=13.6 / align-items:flex-start vs .g-mat center / .g-step-head .5rem=8 (kon_guide_v2.css:89,95), the 22.3-F chrome values (.g-sectiontitle:48, .g-md-v:289, .g-empty:293, .g-progress:184-186 — all exact), the token insert point design_tokens.dart:654 + every reused 22.1 token present (gInk:535gDisc:551=30, gMuted:537), and the TimelineCircle test group (guide_atoms_test.dart:108-127, next group at :129) + timeline_circle.dart path. Zero new misses. Convergence signal: the first re-review's misses were all in summary-trusted spots; a clean pass over the remaining summary-trusted set means that set is now empty — every file:line in this doc has been opened directly. Honest estimate: ~94% one-pass (recovered from the ~90% low after the corrections, but deliberately NOT back to the over-stated ~99% — humility tax for the spike-style miss). The remaining risk is narrow and named: the thumb→primary-media open for doc/other media (web new-tab; Flutter no-op flagged), the .g-mat:last-child trailing-hairline delta, and network_image_or_svg/design_tokens becoming unused imports (verify-and-drop). DEC-22.3.3 is RESOLVED — operator re-confirmed keep-disclosure parity (2026-06-29); .6 qty-trailing and .8 drop-inline-player also confirmed + web-faithful. No open forks remain — build-ready.

NEXT → 22.4

Lightbox shell + cook: opaque scrim, g-band brand masthead, g-md metadata dispatcher (incl. humanize the PT30M/1440 min raw duration flagged in 22.2), cook content reusing these atoms + the flat .g-grouphdr group header (distinct from GuideSection).


UTS 22.3 — measured parity punch-list

Produced by an exhaustive computed-style audit (web getComputedStyle for every .g-*/.o-block-* guide class vs the actual Flutter code + resolved tokens), 5 per-surface diff agents + 1 adversarial completeness critic. This is the list the eyeball reviews kept missing. Deduplicated below.

🔴 HIGH — white-label brand leaks (hardcoded blue → must be site brand) + structural

# Element Issue Flutter loc Fix
H1 .g-step-head child-order Title must be the first child IN the head row, inline before qtype+duration (Title multiple choice 5 min). Flutter renders StepBadgeRow (qtype+dur) then a separate title line below → order inverted + title ejected from head. step_block.dart 4 paths (115/216/253/332); step_collection_block.dart :171-182, :421-437; step_badge.dart Build ONE head Wrap (center, spacing 8) = [title 14/w600/gInk, qtype, dur]; remove the separate title block; head marginBottom 2.4.
H2 .g-check input accent accent-color = --g-brand; Flutter activeColor #1565C0 (blue). Brand leak. step_block.dart:488; step_collection_block.dart:632 activeColor: Theme.of(context).colorScheme.primary.
H3 .g-opt--selected border+bg+icon = --g-brand / brand-6%-tint; Flutter #E3F2FD+#1565C0 (blue). Brand leak on every MC/TF pick. answer_option_row.dart:89-95 selected → primary border+icon, gBrandTint(primary,.06) bg; plumb context into _stateStyle.
H4 Flashcard (.g-flash*) Entire variant unmigrated: purple card_heading + blue arrow_return_right icons, blue reveal, front w500/15.2 (web 400/15), back #555/15.2 (web gBody/14), solid divider (web 1px dashed gLine2), front bg #FAFAFA (web gTint). Brand leak + many. flashcard_panel.dart (105-119,139-141,154-166,210-231); tokens 854-866 Migrate to g-: drop icons; front 15/gInk/400 on gTint; back 14/gBody/h1.6; reveal = gBrandInk(primary)/13; dashed gLine2 divider; explanation as sibling below the card.

🟠 MEDIUM — visible

# Element Issue Flutter loc Fix
M1 .g-step-text size web 15px (I wrongly set 16). step_block.dart:135,273; step_collection_block.dart:187,442 Add gStepTextFontSize=15; use it (not gStepBodyFontSize=16).
M2 .g-step-text color web gBody #3a3a3a (I set gInk #222). step_block.dart:136,274; step_collection_block.dart:189,443 color → gBody.
M3 .g-step-text line-height web 1.6 (Flutter 1.7). step_block.dart:136,274 height 1.6.
M4 .g-mat body→qty gap missing 12px flex gap (only the 6.4 qty pad-left). material_row.dart:93-95 SizedBox(gMatRowGap=12) before the qty block.
M5 .g-mat:last-child border last row in a group + standalone draw an extra hairline. material_row.dart:37-40; material_item_block.dart:18 isLast flag → suppress bottom border on group's last row + standalone.
M6 .g-acc grouping web opens a new section on each consecutive run change ([A,A,B,A]→3); Flutter keyed-map merges ([A,A,B,A]→2). material_collection_block.dart:64-86 group by consecutive run, not a name-keyed map.
M7 completed collection step web does nothing (only greens disc+label); Flutter Opacity(0.65) + strikes body. step_collection_block.dart:416-417,445 remove opacity + body strike.
M8 completed standalone step strike belongs on title not body; remove opacity dim. step_block.dart:138-139,466-468,120-131 strike+gMuted on title; un-strike body; drop opacity.
M9 .o-block-progress-complete banner web = left flex, check icon, 3px gOk left border, right-only radius, bg #e9f5ee, w600; Flutter centered, no icon/border, w500. step_collection_block.dart:743-769 rebuild per web; verify injected text.
M10 reset links (Try again/Reset progress) web = borderless brand-ink text links; Flutter gray OutlinedButton+icon. step_block.dart:510-524; step_collection_block.dart:682-712 TextButton, gBrandInk(primary), no border/icon.
M11 .g-opt--locked opacity web 1.0; Flutter 0.85 dim. answer_option_row.dart:54-57 opacity 1.0.
M12 .g-opt default border web 1px gLine2; Flutter transparent. answer_option_row.dart:82-88,110-116 gLine2 border.
M13 .g-submit text + style "Submit Answer" (not "Submit"); brand bg/white/radius6/pad 7.2·16/w600/13. step_block.dart:418,411-419; step_collection_block.dart:558-566 label + full style.
M14 .g-md-v line-height + margin web height 1.5, margin-bottom 0; Flutter no height + 12px pad. step_collection_block.dart:254-261; material_collection_block.dart:47-54 height 1.5; drop bottom 12.
M15 .g-step-media margin-top web 8.8 (.55rem); Flutter 12. step_block.dart:144,280; step_collection_block.dart:451,656 8.8.
M16 .g-step vertical padding standalone _stepHead Row has 0; web .85rem 0 = 13.6. step_block.dart:197-205 wrap Row in Padding(vertical: gStepPadV).
M17 .g-step-head gap + margin-bottom web gap 8 / mb 2.4; Flutter 6 / 8. step_badge.dart:32-36 gap 8, mb 2.4.
M18 .g-progress height + radius web 4 / 2; Flutter 6 / 3. step_collection_block.dart:278,274-276 gProgressHeight 4, gProgressRadius 2.
M19 material-collection error state collection==null → web .g-empty "Material collection not found"; Flutter shrink. material_collection_block.dart:21-22 render the empty box.
M20 gallery .g-strip reveal pad-top web 6.4 above tiles; Flutter 0. guide_gallery_disclosure.dart:84-106 top inset 6.4 (+ existing gStripPadBottom 4).

🟡 LOW — sub-pixel / cosmetic / data-driven

.g-mat-opt gap 6 not 10 (material_row:63 + material_type_badge:32) · material name/note height:1.5 (material_row:66,82) · name baseline-align vs Wrap-center (material_row:61) · .g-acc-sum pad 9.6/11.2 + h1.5 + ls .52 (guide_section:39,52) · .g-sectiontitle h1.5 (material_collection:36) · doc thumb/tile MIME-specific Bootstrap icon vs generic file_earmark (material_thumb:134; guide_gallery_disclosure:196) · chevron size (guide_gallery_disclosure:65) · g-gal margin-top 6.4 (vs 6) + summary pad 2.4 (vs 2) (design_tokens 646,650) · checkbox label 13/gMuted/box16/gap7.2/mt10.4 + keep label "Mark as done" (not "Done") (step_block:464-503; step_collection:611-647) · drop the anon-prompt box (web has none) (step_block:178,369,555) · answer-option metrics font14/icon~14/gap9.6/pad8.8·11.2/mb6.4 (answer_option_row:35-71) · flashcard reveal text "Tap to reveal" + font13/pad9.6 (flashcard_panel:161) · null-material group registers an empty section (material_collection:67-76) · .g-empty dashed (not solid) border (material_collection:94) · per-block mb-4 24px — verify ContentBlockRenderer inter-block spacing first.

Notes

  • M1–M3 correct my own earlier over-fix (body 16/gInk/1.7 → 15/gBody/1.6).
  • H2/H3/H4 + M10 are white-label brand leaks (blue/gray hardcoded → must be colorScheme.primary / gBrandInk).
  • Flashcard (H4) + answer-options (H3/M11/M12) + completion banner (M9) + reset links (M10) were never in the 22.3 plan — they're step-block sub-surfaces the plan didn't enumerate, surfaced only by this measured audit.

Convergence log + final residual (2026-06-30)

Everything above (H1–H4, M1–M20, all LOW) was fixed, then the measured audit was re-run to a clean gate. Seven re-audits after the fix pass (≈230 old guide* tokens retired along the way, flutter test 304 green throughout):

round total HIGH MEDIUM LOW what it caught (after the fix)
2 (fix-pass baseline) 63 4 17 42 the full list above
3 45 1 8 36 caught my body-font over-fix + 2 of my wrong fixes
4 58 0 8 50 media-thumb HIGH cleared; surfaced line-heights
5 37 0 8 29 flashcard padding, empty-state, checkbox tap-target
6 41 0 9 32 flashcard disc + banner position (JS-confirmed)
7 42 0 5 37 flashcard .g-tip + collection mark size
8 37 0 5 32 reset-link inline confirmed; instruction-.g-explain gate + collection per-step "Try again" removal

0 HIGH for 5 consecutive rounds. Every fix from rounds 3–8 was verified against the source (block-interaction-compiler.js for the disc/icon behavior, _block_step.gohtml for the flashcard tip, JS insertBefore for the banner) — not the static computed-style dump, which is blind to JS-hydrated state and twice produced false positives (the answer-icon glyph; the doc-thumb size) that source-reading refuted.

Accepted residual (measured, documented — NOT fixed)

These remain after convergence. Each is either a Flutter limitation, out-of-surface, or sub-pixel:

  • Margin-collapse family (Flutter can't collapse adjacent margins like CSS): .g-q-prompt top/bottom (~1.6px), .o-block-step-answers top gap (8 vs context-dependent 8.8/12 — depends on whether a prompt or a media thumb directly precedes the options, which Flutter would have to thread through). Net visible delta ≤ ~4px.
  • mb-4 inter-block gap (24px): every web block wrapper carries Bootstrap mb-4 (24px); the Flutter ContentBlockRenderer uses 16px between blocks. Likely-correct as a global renderer change, but it touches every block type (text/media/…), so it's out of the 22.3 guide-block scope — flagged for a separate pass.
  • No in-app dashed-border util: flashcard reveal divider + .g-empty box render a solid 1px gLine2 (web is dashed). Would need a CustomPainter/package.
  • Out of the guide-block surfaces: the reset-confirmation dialog skin (Flutter AlertDialog vs the g-styled modal) and the post-level "Reset All" button — both live in post-detail / dialog code, not the content blocks 22.3 covers.
  • Enhancement: MIME-specific document icons (pdf/word/excel…) — Flutter shows a generic file_earmark.
  • Renderer-level: .o-block-material-collection mb-4 (24px) vs the renderer's 16px inter-block gap — a ContentBlockRenderer spacing concern, not the block's.
  • Sub-pixel: .g-submit/duration-pill intrinsic Material min-heights (~1–2px taller than the web buttons).

Confidence: ~97% that the guide content blocks match the post-UTS web design. Computed from the measured set — every HIGH/MEDIUM divergence the audit found across 7 rounds is fixed-and-re-measured or listed above as accepted residual; the residual ~3% is the enumerated cosmetic/limitation items, none structural or brand.


Final state — "full parity" pass + formatting cleanup (rounds 9–13, 2026-06-30)

After the operator directed full parity (not "accept the residual"), rounds 9–13 closed nearly all of the above "accepted residual": dashed borders are now a real CustomPaint (guide_dashed.dart), mb-4 is 24px (operator chose keep-and-smoke), MIME-specific doc icons (doc_icon.dart mirrors docIconClass), both reset dialogs match the web modal, the post-level "Reset All" is a brand-ink link, the audio glyph is a real 3-bar equalizer CustomPaint, the checkbox is scaled to 16dp, empty-state boxes render for null step/collection/material, dead MaterialTypeBadge retired, plus a long tail of line-heights/sizes.

Formatting-noise cleanup (the operator caught this)

A stray tree-wide dart format lib/ test/ had reformatted 303 files. Robust detection (does formatting the committed version reproduce the working file?) classified 277 pure-reflow vs 23 functional; the 277 were reverted. Change surface = 24 functional .dart files + 2 new (guide_dashed, doc_icon) + 1 deletion (timeline_circle) — all guide-scoped (+ post_detail_page for mb-4, + pre-existing site_config). 302 green.

Convergence (post-revert)

round total HIGH MED LOW note
11 17 0 1 16 equalizer-stretch (fixed); flagged the lightbox brand leak
12 15 0 1 14 step-block agent: "19/19 classes converge, ~99%, zero white-label leaks"
13 7 0 3 4 material-row + material-collection = ZERO; 2 real mediums = section-title mb-4 collapse (FIXED: gSectionTitleMarginTop 25.6→1.6); the q-prompt top-margin items were critique-confirmed FALSE POSITIVES (head→prompt collapses to 2.4, already matches)

Content blocks: measured-done. 13 rounds; 0 HIGH sustained; material surfaces converge to ZERO; step surfaces ~99%. Residual = the margin-collapse family (≤3.2px, framework limit), one intentional animation, and the head-absent-question prompt-top (questions essentially always carry a qtype → head present). The section-title fix and everything else gets its final verification in the live smoke (real data + spacing).

Accepted residual (measured — genuine framework limits, NOT laziness)

  • Margin-collapse family (Flutter has no CSS margin-collapse): a question prompt's 9.6 bottom collapsing with a directly-following tip (1.6px) or gallery (3.2px), and a .g-sectiontitle first-child collapsing with the prior block's mb-4. The common prompt→media case IS handled (gQPromptMarginBottom). Net ≤ ~3.2px; fixing every adjacency needs per-sibling context threading — disproportionate for the magnitude.
  • Intentional mobile: GuideSection open/close uses AnimatedSize (web <details> snaps).
  • Abnormal-data only: .g-mat-name char-level break for pathological unbroken names (overflow-wrap:anywhere).

🟡 SEPARATE SURFACE — guide LIGHTBOX brand leak (NOT in this content-block pass)

Round 11/12 confirmed the content blocks have zero white-label leaks, but flagged that the guide lightbox (guide_lightbox_step.dart, guide_lightbox_lesson_page.dart) still renders hardcoded per-type badges — instruction blue #1565C0, flashcard purple #4527A0 (DesignTokens.guideBadge*). These won't rebrand per site = a real white-label violation, but in a different surface. Follow-up: bring the lightbox step header to the neutral StepDisc + .g-qtype pattern and retire the dead guideBadge* tokens.

Testing

Unit/widget: 302 green. Static visual: 13 audit rounds. Still needs a live smoke (operator chose keep-mb-4 → smoke a guide post and a mixed-block post, Flutter↔web side-by-side) to exercise interactive states (check/submit/reveal/reset/lightbox), real data, and brand rendering that static analysis can't reach.

← Back to Under the Tuscan Sun