Blog Visual Design Improvement — Visual-Only Design Polish (Palette+Scout Joint Evaluation)
Sprint 72: Blog Visual Design Improvement — Visual-Only Design Polish
Context
blog.algo-su.com (Next.js 15 SSG + Tailwind 3 + MDX) had its content reorganized into 6 topic-based posts in Sprint 68, and in Sprint 70 the 36 ASCII diagrams in the body were converted to 11 React visual components — making the structure, content, and components stable. However, following user feedback ("improve the screen"), visual depth (shadow/hover), typographic hierarchy, spacing rhythm, and token consistency had not been touched.
PM clearly separated the nature of the work: "Not content improvement, but visible design improvement". That is, content additions or new feature creation — MDX body/copy, Hero metric additions, code block copy buttons, dark mode toggle, post navigation, etc. — are out of scope. Only "how existing markup looks" is addressed.
Oracle performed a joint evaluation by two agents instead of deciding alone:
- Palette — Before→After proposals from design token/style perspective (10 sections)
- Scout — Scouting visual weaknesses from a first-time visitor perspective (TOP 12+)
The intersection of both evaluations was adopted as core priority. Items Scout found but with "content/feature addition" character (Hero section, Back nav, dark mode toggle, code block copy button) were excluded from this sprint by Oracle authority and deferred. The result was refined to 6 work units (72-1 ~ 72-6) and ADR (72-D), all processed as 6 local commits.
This sprint is "pure design polishing" unlike "Sprint 71 bug fix", and is also the first case validating that the Palette+Scout joint evaluation can become the standard entry flow for design work in the AlgoSu 12-Agent system.
Decisions
D1: Design work entry flow — Oracle → Palette + Scout joint evaluation
- Context: Design work is difficult to determine by a single agent. Palette knows token/style SSoT but not user experience; Scout claims to know user experience but not the token system. Asking only one side risks (a) arbitrary hardcoding ignoring tokens or (b) token cleanup the user doesn't feel.
- Choice: When a design work trigger occurs, invoke Oracle skill to delegate Palette and Scout in parallel. Palette does code-based evaluation (file:line citation + Before→After description), Scout does build output/markup-based user perspective evaluation (first visitor's impression). Oracle aggregates both results into priority candidates, blocking scope (content/feature addition) violating items, then submits to PM.
- Alternatives:
- PM directly narrows scope with AskUserQuestion and proceeds with single work unit — high PM cognitive load, low agent system utilization. Actually attempted at start of this sprint, then rejected.
- Palette evaluation alone — risk of "token cleanup done but first impression unchanged" without user experience validation.
- Code Paths:
agents/commands/palette.md,agents/commands/scout.md,.claude/commands/algosu-oracle.md - Note: This decision is permanently recorded in memory feedback_design_workflow.md as the standard workflow for future design work.
D2: Full tokenization of hardcoded colors/borders (72-1)
- Context: Tailwind palette color classes used directly —
text-gray-500/600/400,border-gray-200,dark:border-gray-800,text-brand-500/600/700,bg-brand-50— scattered across pages/cards/visual components. Also, all 11 visual components had inline React stylestyle={{ borderColor: 'var(--border)' }}creating a dual path bypassing tokens. - Choice: One-to-one substitution rules for full conversion:
Remove all 14 inline
text-gray-500/600 → text-text-muted text-gray-400 → text-text-subtle border-gray-200 → border-border dark:border-gray-800 → (remove, --border auto-inverts in .dark) text-brand-600 → text-brand text-brand-700 → text-brand-strong bg-brand-50 → bg-brand-soft hover:border-brand-500 → hover:border-brandstyle={{ borderColor/backgroundColor: 'var(--...)' }}occurrences and unify toborder-border/bg-border-strongTailwind token classes. Explicitly addbg-surface text-textto body. - Alternatives: Gradual substitution — risk of hardcoding recurring in new work units. Safer to bring grep to 0 occurrences at once so subsequent work units start from a clean state.
- Code Paths:
blog/src/app/layout.tsx,blog/src/app/page.tsx,blog/src/app/posts/[slug]/page.tsx,blog/src/components/post-card.tsx,blog/src/components/blog/{architecture-map,hierarchy-tree,kv,mermaid,metric-grid,phase-timeline,pipeline,service-grid,tier-matrix,tier-stack}.tsx
D3: Spacing/rhythm redistribution (72-1)
- Context: Header felt cramped with
py-4, and home subtitle had excessive spacing withmb-10, breaking visual rhythm into the body (post card list). - Choice: Header
py-4 → py-6, home subtitlemb-10 → mb-8. Body containermax-w-3xland post listspace-y-6are already good, kept as-is. - Alternatives: Redefine all page spacing at once — high regression risk. Only touch the two most impactful points, leave the rest for 72-2/72-3 per-component realignment.
- Code Paths:
blog/src/app/layout.tsx,blog/src/app/page.tsx
D4: Post header visual hierarchy reinforcement (72-3)
- Context: First screen when entering post detail:
text-3xl font-bold mb-2title +text-sm text-gray-500date +text-xs bg-brand-50 py-0.5tags — visual hierarchy nearly flat. Hard for users to grasp "what this post is, when written, and what category" at a glance. - Choice: 4 simultaneous improvements in single component change:
- h1:
text-3xl mb-2→text-4xl mb-4 leading-tight tracking-tight(letter-spacing + line height + weight) - Tag container:
mt-2 gap-2→mt-4 gap-3(breathing room) - Tag chip:
py-0.5→py-1 font-medium(readability) - Add
border-b border-border pb-8 mb-10at bottom of header block → visual separator from body (prose)
- h1:
- Alternatives: Add hero image — content/asset addition in scope. Add signature — same reason, deferred.
- Code Paths:
blog/src/app/posts/[slug]/page.tsx
D5: Home PostCard multi-axis interaction (72-2)
- Context: Home card has
border-gray-200 p-5 hover:border-brand-500— on hover only color changes, no depth/movement/shadow change. Hard for first visitors to feel "this is a clickable card", and 6 cards are monotonous in the same tone. - Choice: 3-axis hover feedback (border + shadow + translate) + internal hierarchy realignment in PostCard:
- Container:
shadow-smdefault +hover:shadow-md hover:border-brand hover:-translate-y-0.5 transition-all duration-200 - Padding:
p-5 → p-6 - Date promoted to top
text-xs uppercase tracking-wide text-text-subtlemeta - Title:
text-lg font-semibold → text-xl font-bold leading-snug+group-hover:text-brand(card is<a>, use group) - Excerpt:
line-clamp-2 leading-relaxed(stable area) - Tags reuse same tokens as 72-3 (intra-sprint consistency)
- focus-visible ring for keyboard focus visualization
- Container:
- Alternatives:
- Distribute accent colors per card — Palette recommended, but meaning attribution is weak without "latest post highlight". 6 posts all same date makes it unnecessary. Hold.
- Introduce thumbnail images — public directory is empty and asset creation needed, out of scope.
- Code Paths:
blog/src/components/post-card.tsx
D6: Token-based prose body element customization extension (72-4)
- Context: Almost entirely dependent on
@tailwindcss/typographyplugin's.prosedefaults. globals.css had minimal corrections for only 3 types (table/inline code/blockquote), while h2/h3 hierarchy, link underline, strong/em weight, ul/ol markers, hr spacing, etc. were all plugin defaults. Body appeared as "standard markdown with no distinctiveness". - Choice: Add 8 element token-based rules to globals.css
@layer components:- h2/h3/h4: Redefine spacing/size/letter-spacing/weight (emphasize section boundaries)
- p: line-height 1.75
- a:
text-brand+underline-offset 2+decoration 2+transition, hovertext-brand-strong - strong/em: Specify weight/color
- ul/ol/li: Spacing/padding/marker color alignment
- hr: 2.5rem spacing +
border-border - Inline code: padding
0.375rem → 0.5rem, radius/font-size reinforced - blockquote: Left bar
4px → 6px, backgroundsurface-muted → brand-soft(contrast reinforced)
- Alternatives:
- Switch to shiki/Prism for code block syntax enhancement — build tool change, out of scope.
- Introduce dedicated CJK font for prose — web font loading/performance, out of scope.
- Code Paths:
blog/src/app/globals.css(8 additions in@layer components, reinforce existing table/code/blockquote) - Note: All colors reuse existing CSS variables — 0 new token definitions.
D7: Apply shadow-sm default to 4 representative card types (72-5)
- Context: Of 11 visual components, almost no depth. Only ArchService has
hover:shadow-md, no default shadow. After introducingshadow-smto PostCard (72-2), body cards felt relatively flat in comparison. - Choice: Full normalization of all 11 types is excessive scope (L workload) → Apply
shadow-smdefault to 4 representative types (Callout, MetricCard, ServiceCard, ArchService) only. ArchService maintains existinghover:shadow-mdkeeping interactive intensity. In dark mode shadow is weak, but existingborder-bordertoken auto-inverts in.darkto maintain separation via brightness difference. - Alternatives:
- Apply to all 11 — L workload + need to verify visual consistency for each diverse component (Pipeline arrows, TierStack matrix, HierarchyTree tree, etc.). Separate to next sprint.
- Code Paths:
blog/src/components/blog/{callout,metric-grid,service-grid,architecture-map}.tsx
D8: Interaction base layer (72-6)
- Context: Link transitions and keyboard focus visualization not defined globally.
transitionclasses scattered or missing per component, and focus-visible handling only explicitly set in PostCard (72-2). - Choice: Add global
atransition: color 150ms ease+a/button:focus-visible2-step shadow (2px surface offset + 2px brand ring) as ring + offset expression to globals.css@layer base. Implements ring + offset equivalent to Tailwindring-offsetwith CSS only, auto-adapts in dark mode as--surfaceauto-inverts. - Alternatives:
- Introduce shadcn/ui — blog doesn't use shadcn, overkill to introduce framework for single utility addition.
- Tailwind base config extension with utility-only — expressible via
tailwind.config.tsplugin, but direct CSS variable usage is more intuitive.
- Code Paths:
blog/src/app/globals.css(@layer baseextension) - Note: PostCard already explicitly specified focus-visible ring in 72-2 with same expression as global rule — harmless duplication, kept.
Patterns
P1: Oracle → Palette + Scout joint evaluation workflow
- Where: This entire sprint. Role division in
agents/commands/palette.md+agents/commands/scout.mdcalled in parallel for design work. - When to Reuse: When design/UI/visual improvement work arrives. Especially when (a) work scope spans multiple components/pages, (b) it's unclear "where to start", and (c) it's not trivial work like a single typo/color fix. When invoking Oracle skill, assign clear roles to each agent (Palette: code-based token evaluation / Scout: user perspective visual weaknesses) with common constraints (no content changes, no feature additions, plan mode read-only). Oracle organizes the intersection of both results as priority, cross-section as candidate alternatives, blocks scope-violating items, and returns final candidate list to PM. PM only needs to approve priority to immediately proceed with work unit division.
P2: Token SSoT grep 0-occurrence guarantee
- Where: Full substitution across
blog/src/**(D2) - When to Reuse: In projects with design tokens, "do all colors go through tokens?" can only be verified with grep. At work unit end,
rg "text-gray-|border-gray-|dark:border-gray-|brand-(50|500|600|700|900)" src/result must be 0 occurrences for token SSoT to be intact. Inlinestyle={{ borderColor/backgroundColor: 'var(--...)' }}must also be 0 occurrences (becomes a path bypassing Tailwind tokens). This sprint unified it into work unit 1 so subsequent work units start from a clean state.
P3: Multi-axis hover feedback (border + shadow + translate)
- Where:
blog/src/components/post-card.tsx(D5) - When to Reuse: When giving hover feedback to clickable cards/panels/list items, single-axis change (color only) is weak for users to perceive "this is interactive". Bundling 3-axis simultaneous changes (border color + shadow intensity + translate position) into
transition-all duration-200maximizes interactive intensity with depth/movement/highlight happening simultaneously. Even when shadow weakens in dark mode, other 2 axes maintain feedback, stable in both light/dark. Adding focus-visible ring provides same intensity for keyboard users.
P4: prose customization — token reuse first
- Where:
blog/src/app/globals.css@layer components(D6) - When to Reuse: When using
@tailwindcss/typographyand want to apply brand color/dark mode/readability to body, directly defining.prose child selectorsin globals.css@layer componentsis more intuitive and advantageous for token reuse than touching prose's variable system (--tw-prose-*). Key principle: no new CSS variable definitions, use existing tokens only. Colors reference directly asvar(--brand)/var(--text-muted)for dark mode auto-adaptation. Applyline-height 1.75,letter-spacing -0.01em. Reinforce blockquote with thicker left bar (4px → 6px) and background (surface-muted → brand-soft) for contrast.
Gotchas
G1: inline style={{ var('--token') }} bypasses token system
- Symptom:
tailwind.config.tshasborder-border/bg-border-strongtokens mapped, but 11 visual components reference CSS variables directly with inline React stylestyle={{ borderColor: 'var(--border)' }}. Result: token usage not visible via Tailwind class grep, hard to track impact scope on design token changes. - Root Cause: During initial component writing, Tailwind token mapping was insufficient or author bypassed with CSS variable without knowing
tailwind.config.ts. Once embedded, permanently invisible via grep. - Fix: Remove all 14 occurrences in work unit 1.
border-border/bg-border-strongalready mapped intailwind.config.ts, class substitution alone is sufficient. Userg "style=\{\{" blog/src/components/blogbeing 0 occurrences as gate going forward. - Lesson: In projects with design token SSoT policy, blocking inline styles in code review is safest. Can be enforced with ESLint
react/forbid-component-props+@typescript-eslint/no-restricted-syntaxrules. Didn't introduce rules in this sprint but worth future consideration.
G2: Shadow invisible in dark mode
- Symptom: Even after applying
shadow-smto PostCard / Callout / MetricCard, shadow is barely visible in dark mode on dark surface — appears nearly flat. - Root Cause: Shadow fundamentally expresses depth as "darker area than background", but dark backgrounds have little room to go darker.
dark:shadow-*variant doesn't create significant brightness difference. - Fix: Accept shadow as light-mode-only depth cue; in dark mode express separation via border brightness difference (
border-borderauto-inverts togray-800/gray-900tone in.dark) and card background surface-elevated. Leave shadow class dark variant as-is — weak but harmless. - Lesson: Dark mode card design should be validated by "does it separate even without shadow?". If shadow is the only depth cue, it will appear flat in dark mode. Prioritize border token brightness difference or surface layer difference (surface / surface-muted / surface-elevated).
G3: Dependence on prose defaults creates "characterless body"
- Symptom: Body appears standard markdown with no personality/brand color when only
.proseclass is applied. Especially weak h2/h3 hierarchy, links use only prose internal colors, and blockquote has flat plugin-default gray left bar. - Root Cause: Prose plugin is intentionally neutral by design as a "general safety net". Host sites must separately apply brand colors and emphasis effects.
- Fix: Directly redefine 8
.prose child selectorsin globals.css@layer components(see D6 P4). Approach of reinforcing missing hierarchy and emphasis rather than overriding prose defaults. All colors reuse existing tokens for dark mode auto-adaptation. - Lesson: Adopting the prose plugin doesn't mean "body design done". Plugin is basic safety net; brand distinctiveness must be additionally applied in host CSS. Treating "h2/h3, a, blockquote, code, hr — customize these 5 types at minimum" as a checklist from first introduction is safer.
G4: PM's "content vs design" boundary awareness
- Symptom: Scout found "Hero/overview summary absent", "next post card absent", "code block copy button absent", "dark mode toggle absent" — all visual weaknesses, but solutions require content additions or new Client Component creation/feature additions. Conflicts with PM's explicitly stated scope "not content, but visible design".
- Root Cause: Goal of "better-looking screen" implies both (a) styling existing markup and (b) modifying/adding markup structure. Proceeding without distinction leads to infinite scope expansion.
- Fix: Oracle separates "content/feature addition vs pure styling" into a binary at the evaluation synthesis stage. Former completely deferred to next sprint, latter only divided into work units. When submitting to PM, explicitly provide "deferred items list" alongside to document scope agreement.
- Lesson: For design work requests, agree with PM that "visible design = keeping markup/content as-is, styling only", and Oracle must explicitly block when user-perspective agent (Scout) proposals exceed scope. Leaving this boundary decision itself in ADR (D1, G4) establishes it as the standard separation line for future design work.
Metrics
- Commits (AlgoSu): 6 (
b272b7a..bdc4cd3)b272b7achore(blog): color/border tokenization + header/home spacing rhythm readjustment (72-1)cf120f1style(blog): post header typographic/spacing redesign (72-3)02907c2style(blog): PostCard visual hierarchy/interaction reinforcement (72-2)2c23302style(blog): prose body element style extension (72-4)24a4253style(blog): card component shadow consistency cleanup (72-5)bdc4cd3style(blog): global transition/focus-visible addition (72-6)- (+ 1 ADR commit planned)
- Files changed (AlgoSu): 16 files
- Pages (3):
blog/src/app/layout.tsx,blog/src/app/page.tsx,blog/src/app/posts/[slug]/page.tsx - Card (1):
blog/src/components/post-card.tsx - Visual components (11):
blog/src/components/blog/{architecture-map,callout,hierarchy-tree,kv,mermaid,metric-grid,phase-timeline,pipeline,service-grid,tier-matrix,tier-stack}.tsx - Global CSS (1):
blog/src/app/globals.css - Documentation (1):
docs/adr/sprints/sprint-72.md(this file)
- Pages (3):
- Files changed (aether-gitops): 0 (no blog manifest changes — only image rebuild needed)
- Lines: ~+153 / -84 (code only; excluding ADR)
- Build:
cd blog && npm run buildpassed after each work unit end (10 pages SSG, First Load JS 103-104 kB maintained) - New external dependencies: None
- New CSS variables / Tailwind tokens: None (100% existing token reuse)
Follow-up Recommendations (outside Sprint 72 scope)
Add to MEMORY.md follow-up section:
- Dark mode toggle UI introduction — Dark mode is technically implemented but no toggle entry point, users cannot activate it. Introduce
next-themes+ sun/moon icon toggle in header + localStorage persistence. New Client Component required. - Code block language label + copy button — New Client Component (
code-block.tsx). Can also enrich syntax highlighting if reviewed together with shiki/Prism transition. - Hero / overview summary section — "67 sprints / 2,432 tests / 12 agents / 6 services" metric cards on home first screen. Content addition so separate PM decision needed. MetricGrid component can be reused.
- Post → list back nav, next/previous post card — "← Blog Home" at top + "Next post" card at bottom of post detail page. Information architecture change so separate work unit.
- Visual component 11 types full shadow normalization — This sprint only processed 4 types (Callout/Metric/Service/ArchService). Remaining 7 types (Pipeline/TierStack/TierMatrix/PhaseTimeline/HierarchyTree/Mermaid/KV) deferred to next sprint. Visual consistency verification needed for each component.
- inline
style={{ }}ESLint rule introduction — Block token system bypass (G1 lesson). Enforce withreact/forbid-component-propsor custom rule. - nginx trailing slash pattern enhancement — Enhance
blog/nginx.conftry_filespattern to$uri $uri.html $uri/index.html =404. Sprint 70 remaining item, already registered in MEMORY.md.