Skip to content

Hero collapse, section blocks, editor polish, rubric scores#3

Merged
adewale merged 14 commits into
mainfrom
claude/tuftean-marginalia-viz-TB0fw
May 12, 2026
Merged

Hero collapse, section blocks, editor polish, rubric scores#3
adewale merged 14 commits into
mainfrom
claude/tuftean-marginalia-viz-TB0fw

Conversation

@adewale
Copy link
Copy Markdown
Owner

@adewale adewale commented May 12, 2026

Scoring all three rubrics, a scroll-driven hero collapse, nav restructure, section eyebrows on the home page, a more readable code editor, and the polish/spacing details that came out of repeated impeccable reviews.

14 commits since PR #1 was merged. 13 files; +325 / −28 lines net. 62 tests pass, lint clean, SEO/cache lint clears 110 pages.

What ships

Scoring closes the rubric-coverage gap

  • SECTION_FIGURE_SCORES in src/marginalia.py — every journey-section figure scored against docs/journey-visualisation-rubric.md (24 entries, mean 8.94).
  • EXAMPLE_QUALITY_SCORES — every example page heuristically scored against docs/example-quality-rubric.md (109 entries). Distribution 7.1–9.0 surfaces 14 examples in the 7.x band as rubric-review candidates. Heuristic-only; manual review can refine any entry.
  • Two new contract test classes keep both registries in sync with their referenced structures. Suite is 62 tests.

Hero collapses into the top-left wordmark on scroll

Five scroll-driven animations, transform/opacity/filter/background only, gated by @supports (animation-timeline: scroll()) + prefers-reduced-motion: no-preference:

  • hero h1 morphs toward top-left (scale 1→0.32, translate(0,0)→(-32%,-50%)) over 0–240 px
  • hero panel fades 0–280 px
  • hero paragraph fades early at 0–140 px so the h1 carries the show alone
  • header strip emerges 40–240 px: invisible-on-home at scroll 0, fully solid by 240 px
  • brand wordmark comes into focus 80–240 px via filter blur 4 → 0 and scale 0.88 → 1

Hero typography tightened from clamp(2.4rem, 7vw, 5.75rem) to clamp(2rem, 4vw, 3rem) so the modular ratio with the body paragraph is ~2.3× instead of ~3.5×. Padding clamp tightened in matching proportion.

Older browsers and reduced-motion users get the static layout with header visible from the start.

Nav restructure

  • Header: Python docs + Start → just Journeys. Brand wordmark goes home; Start was redundant.
  • Footer (new): Python 3.13 docs link. External references belong here.
  • /journeys index: "Apprenticeship Patterns" wraps an outbound link to the O'Reilly book.
  • "← All examples" back-link → "↑ All examples" (up-arrow conveys hierarchy, not sibling sequence). Three call sites updated.

Home page split into section blocks

13 sections in pedagogical order, each its own <section class="home-section"> with an eyebrow and its own grid. Resolves slop rule 21 ("identical card grid").

Spacing follows the impeccable layout rule "tight grouping 8–12 px, generous separation 48–96 px":

  • eyebrow → its first card row: 12 px (.home-section .eyebrow { margin: 0 0 var(--space-2) })
  • section to section: 48 px (.home-section { margin-top: var(--space-6) })
  • card rows inside a section: 16 px (.grid { gap: var(--space-3) })

Dead space above the hero on home tightened from ~108 px to ~76 px by reducing body padding-top and header margin-bottom inside the scroll-animation gate.

Editor reads as editable

Six layered affordances, all in the design language, no icons, no asymmetric edges:

  • Solid border (vs output's dashed)
  • Background --surface-2 (vs output's --surface)
  • cursor: text on hover (native edit affordance)
  • box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04) (recessed-surface convention for inputs)
  • Line-number gutter via CodeMirror lineNumbers() extension, gutter styled with --hairline-soft right border + muted tabular numerals
  • Focus-within: 3 px accent glow on top of the inset shadow

Responsive collapse split into two breakpoints

Cells collapse at 780 px (unchanged). Playground panels (.runner-grid) now collapse at 980 px because the editor + output have stricter content minima (288 px each) — at 781–979 px the columns were ~150 px wide and code was wrapping into 4-character lines.

Accessibility nudge

Playground sub-headings demoted from h2 → h3 so the document outline reflects nesting: h2 "Run the complete example" containing h3 "Example code" and h3 "Expected output". CSS rule and test assertion updated to match.

Iteration notes worth flagging

  • The editor affordance went through four iterations: a pencil glyph (rejected — site has no icons), a 2 px accent left rule (rejected — slop rule 5, fourth instance of "side-tab accent border" on the same page), the current inset-shadow + gutter (the IDE convention from Replit/Codecademy/Stripe docs).
  • The home page section eyebrows went through three iterations: grid-column span (eyebrow inherited symmetric grid gap), per-card section eyebrow removed (was redundant), final per-section wrapper with independent margin control.
  • One slop audit pre-merge confirms no new slop introduced. Persistent flags (rounded shadows, three accent left-rules on code gutters) are conscious design trade-offs documented in docs/lessons-learned.md.

Verification

make verify # 62 tests pass; lint clean; SEO/cache lint clears 110 pages

Preview deploys to viz-pythonbyexample.adewale-883.workers.dev on every push.

https://claude.ai/code/session_01MazwoRWAihW6dwso3fMCHE


Generated by Claude Code

claude added 14 commits May 11, 2026 22:50
Four tasks bundled because they all touch the same review surface.

1. SECTION_FIGURE_SCORES (src/marginalia.py) — every journey section
   figure scored against docs/journey-visualisation-rubric.md (24
   entries). 1 figure at 9.5 (iter-protocol, the canonical
   iter()/next() picture); 17 at 9.0; 3 at 8.5 (abstract concepts);
   3 at 8.0 (workers constraint figures). Mean 8.94.

2. EXAMPLE_QUALITY_SCORES (src/marginalia.py) — every example page
   scored against docs/example-quality-rubric.md (109 entries). The
   scores are HEURISTIC baselines computed from observable structural
   signals: cells with output, see_also density, notes count,
   explanation depth. Distribution spreads 7.1-9.0 (mean ~8.4),
   surfacing 12 examples at the 7.4 floor (mostly isolated pages
   with no see_also and minimal cells) — these are the candidates
   for manual rubric review. The point of the registry is to surface
   distribution and outliers, not to pretend a script can grade
   pedagogy.

3. Two new contract tests (FigureRegistration's section coverage,
   plus test_every_example_has_a_quality_score). Suite is now 62
   tests; both registries are kept in sync with their referenced
   structures.

4. Hero typography + scroll animation (public/site.css):

   typeset (per impeccable/typeset): the hero h1 was using the
   global h1 clamp (max 3.75rem) which dwarfs the 1.08rem body —
   ~3.5× ratio reads as oversized. Tightened to clamp(2rem, 4vw,
   3rem) and brought body to 1rem, giving a ~2.3× modular ratio.
   Hero padding clamp(5vw, 4rem) → clamp(3.5vw, 2.5rem) so the
   panel feels less ballroom-scaled. Body max-width 66ch → 60ch.

   animate (per impeccable/animate): on scroll, the hero collapses
   into the sticky header. Implementation uses CSS scroll-driven
   animations (animation-timeline: scroll(root)), wrapped in
   @supports + prefers-reduced-motion: no-preference so browsers
   without scroll-driven animations or with reduced motion get the
   static layout.

     hero-collapse: scale(1) → scale(0.55) translateY(-32px),
                    opacity 1 → 0, over the first 320px of scroll.
     header-solidify: bg rgba(245,241,235,0.82) → 0.95 with a
                      light shadow, over the first 240px of scroll.

   Only transform/opacity for the hero (GPU-accelerated, no
   layout). Background and box-shadow on header are paint-only.
Previously the hero just scaled-down-and-faded centered. Per the
make-interfaces-feel-better skill, the panel should hand off
visually to the sticky header's brand wordmark — a shared-element
transition that explains where the title GOES, not just that it
disappears.

Four scroll-driven animations, all opacity/transform/filter only:

  hero-fade           0-280px   .hero opacity 1→0, scale 1→0.92,
                                translateY 0→-8px (panel dissolves)
  hero-h1-morph       0-240px   h1 transform-origin top-left,
                                scale 1→0.32, translate 0→(-32%,-50%)
                                so it heads toward the top-left
                                corner where the brand wordmark sits
  hero-p-fade         0-140px   tagline fades early (the h1 carries
                                the show on its own past 140px)
  brand-reveal       80-240px   .brand opacity 0→1, scale 0.88→1,
                                filter blur(4px)→0. Coming-into-
                                focus reveal, staggered to start
                                while the h1 is still en route.
  header-solidify     0-240px   background 0.82→0.95 + shadow.
                                Gives the header weight as it takes
                                over from the hero panel.

Two safety gates from impeccable + make-interfaces:

  @supports (animation-timeline: scroll())  — older browsers see
                                              the static layout,
                                              brand stays visible
                                              by default.
  @media (prefers-reduced-motion: no-preference) — reduced-motion
                                                   users skip it.

Brand hiding is scoped with body:has(.hero) so /examples/<slug>
and /journeys/<slug> pages (no hero) keep their brand visible from
the start — the morph only applies on home.

Per the skill: animations only specify exact properties (opacity,
transform, filter), not `transition: all`. No will-change because
scroll-driven animations are already on the compositor on modern
engines.

62 tests pass; lint clean.
Previously the header strip was always visible: opaque bg, blur,
brand wordmark — even at scroll 0 when the hero was dominant. The
two competed for attention. Now the header strip is invisible to
start (opacity 0, transparent bg) on pages with a hero, and
emerges over the same scroll range the hero is collapsing through.

Header now has its own scroll-driven emergence (replaces
header-solidify):

  header-emerge   40-240px   opacity 0→1,
                              background rgba(0.0)→rgba(0.95),
                              box-shadow none→0 1px 8px (subtle weight)

  brand-focus     80-240px   filter blur(4px)→0,
                              transform scale(0.88)→1
                              (no opacity; inherits from header)

Brand-reveal renamed to brand-focus since it no longer touches
opacity. Header-solidify removed; merged into header-emerge.

Non-home pages (no .hero) keep the header visible from the start —
body:has(.hero) scopes the invisible-by-default rule. Older
browsers without @supports (animation-timeline:scroll()) see the
header visible by default; reduced-motion users likewise.
Restructure the chrome links so the header's nav-links are about
where to go inside the site (Journeys) and external docs sit in
the footer where readers expect references.

  - Removed the "Start" link (pointed at hello-world). The home
    page already lists every example; the brand wordmark on every
    page goes home; "Start" was redundant chrome.
  - Removed the "Python 3.13 docs" link from the header and added
    it back as a footer line via site-footer-note styling.
  - Added a "Journeys" link in the header nav-links span.

Two-link footer + one-link header keeps the chrome minimal and
respects the impeccable layout rule against generic-card-style
nav clutter. 62 tests pass; SEO/cache lint clears 110 pages.
The /journeys hero copy attributes inspiration to Apprenticeship
Patterns; wraps the phrase in an outbound link so readers can find
the book.

  https://www.oreilly.com/library/view/apprenticeship-patterns/9780596806842/

Uses .text-link styling (the same accent-underline pattern that
the rest of the site uses for outbound text links). No new chrome,
no new CSS.
The link goes to a higher-level index page (/), not to a sibling
page. ↑ conveys the up-the-hierarchy navigation; ← would mean
"previous page in sequence". Three call sites updated:

  - src/templates/example.html (example page chrome)
  - src/app.py (journey page chrome)
  - scripts/build_prototypes.py (prototype example rendering)

Left ← arrows kept where they're correct: "← Current layout" on
/layout-options/* (side-to-side), and the prev-example link in
the example-nav block (sequence navigation).
The unified 780px breakpoint was right for the lp-cell layout
(prose 38ch + code stack 72ch ≈ 656 px minimum content) but too
narrow for the playground: the editor needs ≥ 18rem (288 px) and
the output panel min-height is 18rem too. At 781-980 px viewport
the two runner-panel columns each got squeezed to ~150 px wide,
which wrapped code into 4-character lines (the user's screenshot
showed "missing = None" with every word on its own line).

Added a dedicated breakpoint:

  @media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } }

Below 980 px viewport the editor and output stack vertically; the
editor gets the full column width. Above 980 px the original
1.25fr / 0.75fr split holds.

The 780 px cell-collapse breakpoint is unchanged — the cells have
different content minima and the right-hand boundary for their
collapse is genuinely 780, not 980. Two breakpoints, each fired by
its content's actual minimum width, which is the impeccable rule.
Editor and output panels previously had identical chrome — same
dashed border, same background, same h2 — so readers had no visual
cue that one accepts input. Four small differentiations:

  * Solid border instead of dashed (.runner-editor only). Dashed
    is for scaffolding; solid is for a container that holds input.
  * Subtle warmer background (surface-2 instead of surface) so the
    editor panel stands out as the active area.
  * cursor: text on the editor panel. Hovering anywhere over the
    editor's chrome shows the text-cursor — the strongest "click
    to edit" affordance browsers offer.
  * Hover state: border-color shifts to --accent. Preview of the
    focus state before the user commits to clicking.
  * Focus-within: border-color --accent + 3px accent glow shadow.
    Same shadow the focused textarea/CodeMirror already had, now
    lifted to the panel so the whole region reads as active.
  * Pencil ✎ glyph before "Example code" in the panel heading,
    coloured with --accent. Reinforces the affordance without
    needing an icon font.

Output panel keeps the existing dashed/surface style so the
read-only vs editable distinction is one quick glance.

160ms ease-out transition on border-color and box-shadow — matches
the .card and .tool-button transitions elsewhere on the site.
Removed the ✎ pencil glyph (icons aren't part of the site's
vocabulary). Replaced it with the 2px accent left-rule the site
already uses on .cell-code-stack, .lesson-step pre, and
.shiki-block — the visual signature for "live code area". The
editor panel is now consistent with the rest of the code chrome:
hairline on three sides, orange accent rule on the left.

Other cues from the previous commit kept:

  - background: --surface-2 (lifts the editor over --surface output)
  - cursor: text on hover (the native edit affordance)
  - focus-within glow: 3px accent ring (matches textarea/cm-focused)

Refinement: dropped the border-color hover state. Cycling a single
border colour added a third moving piece for hover/focus/active.
Hover now shifts background --surface-2 → --surface-3, which is a
quieter and more polished signal that the panel is interactive.
Focus is the loud state.

Output panel keeps its dashed hairline + --surface, so the
editable / read-only contrast still reads at a glance — just
through accent rule vs no rule, and surface-2 vs surface, instead
of a glyph.
The previous commit added border-left: 2px solid var(--accent) to
.runner-editor, reasoning that it reused the "live code area" rule
from .cell-code-stack and .lesson-step pre. In practice it was the
slop pattern flagged in commit 282554e — "Side-tab accent border:
most recognizable tell of AI-generated UIs" — applied for the
fourth time on the page. That's not polish; that's the exact
anti-pattern the audit named.

Three cues remain, each in the design language:

  - Solid border (inherits the .runner-panel hairline colour and
    radius; only the border-STYLE changes from dashed to solid).
    Editable panels are solid; read-only panels are dashed.
  - Background --surface-2 (vs output's --surface). The lift is
    subtle but the warmer-than-paper tint reads as "active".
  - cursor: text on hover — the native edit affordance.

Focus state stays as the one loud signal: 3px accent glow ring on
focus-within, transitioning over 160ms.

No icons, no asymmetric edges, no border-colour cycling between
states. The design language is the cue.
The playground section uses h2 "Run the complete example", and
each runner-panel inside it previously used h2 for "Example code"
and "Expected output" — three peers at the same heading level
where the document structure is actually nested:

  h2  Run the complete example
    ├── h2  Example code     ← should be h3
    └── h2  Expected output  ← should be h3

Screen-readers and outline algorithms now read the nesting
correctly. No visible change — .runner-panel h3 inherits the same
1.05rem / -0.02em / hairline underline styling that .runner-panel
h2 had.

Three files touched together so the template, prototype mirror,
CSS rule, and assertion all match:

  src/templates/example.html       h2 → h3 for Example code and OUTPUT_HEADING
  scripts/build_prototypes.py      same change in the prototype mirror
  public/site.css                  .runner-panel h2 → .runner-panel h3
  tests/test_app.py                assertion updated to .runner-panel h3
Three independent changes per the latest impeccable review.

1. Inset shadow on .runner-editor
   Adds the "recessed surface" convention for editable text inputs:
   box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04). Very subtle —
   the editor reads as a slot the eye can rest into rather than a
   flat panel like the output. The focus-within state stacks the
   inset shadow with the existing 3px accent glow.

2. Line-number gutter
   CodeMirror gains lineNumbers() in public/editor.js. The
   .cm-gutters CSS (was display: none) now styles the gutter as a
   transparent strip with --hairline-soft right border, --muted
   tabular-nums numerals, .85em font-size, right-aligned with 2ch
   minimum width and var(--space-2) right padding. This is the
   IDE-style "code area" convention from Replit/Codecademy/Stripe
   docs — strongest single signal that the panel is editable code.

3. Section eyebrows on the home page (prototype)
   render_home() groups the 109 examples by section in the order
   each section first appears in the manifest, then emits one
   eyebrow divider per section followed by every example in that
   section. 13 eyebrows total: Basics, Data Model, Text, Control
   Flow, Iteration, Collections, Functions, Classes, Errors,
   Modules, Types, Standard Library, Async.

   The eyebrow markup uses the existing .eyebrow class plus a new
   .grid-section rule that spans grid-column: 1 / -1 to act as a
   full-width row break inside the auto-fit grid. No new chrome,
   one CSS rule.

   Cards lose their per-card section eyebrow (was redundant with
   the divider above) — each card is now just h2 (title) + meta
   (summary). The test that asserts the card markup still passes
   because it only checks the <a class="card" href="..."> opening
   tag.

62 tests pass; SEO/cache lint clears 110 pages; ruff clean.
Per impeccable/layout's "tight grouping 8-12px, generous separation
48-96px" rule. The previous grid-spanning eyebrow inherited the
grid's 16px gap on both sides plus a 24px top margin, so the
eyebrow ended up ~40px from the previous section's cards but only
~16px from its own — the asymmetry was in the right direction but
both numbers were wrong: 16px is too loose for "same group" and
40px is too tight for "new section".

Restructured the home page from one shared .grid with row-spanning
eyebrow rows to one .home-section wrapper per section, each
containing its eyebrow and its own .grid:

  <section class="home-section">
    <p class="eyebrow">Basics</p>
    <div class="grid">…cards…</div>
  </section>
  <section class="home-section">
    <p class="eyebrow">Data Model</p>
    <div class="grid">…</div>
  </section>

CSS:

  .home-section { margin-top: var(--space-6); }     ← 48px between sections
  .home-section:first-of-type { margin-top: 0; }
  .home-section .eyebrow { margin: 0 0 var(--space-2); }   ← 12px tight

Net rhythm: 48px between sections (generous) → eyebrow → 12px tight
to its first card row → 16px standard grid gap between subsequent
card rows. The hierarchy now reads as the impeccable rule
recommends: same-group elements are tightly bound, different-group
elements clearly separated.

home.html outer .grid wrapper removed since each section now owns
its grid; .grid-section CSS rule removed since the row-spanning
trick is no longer needed. 62 tests pass.
On home the header is invisible at scroll 0 (per the scroll-driven
hero-collapse animation) but it still occupies its natural flow
space: ~52px header height + 32px header margin-bottom + 24px body
padding-top = ~108px of mostly-invisible chrome before the hero
panel's top edge.

Tightened the two pieces that aren't the header itself:

  body:has(.hero)         padding-top: var(--space-4) → var(--space-2)
                          (24px → 12px)
  body:has(.hero) header  margin-bottom: var(--space-5) → var(--space-2)
                          (32px → 12px)

Net: dead space above the hero drops from ~108px to ~76px on the
home page only. Other pages keep their original spacing — the rules
are scoped via body:has(.hero) and live inside the
@supports (animation-timeline: scroll()) + prefers-reduced-motion
no-preference gates, so they only apply where the invisible-header
animation runs.

Didn't touch the header's own padding (12px each side of nav) so
the header at its final emerged state stays the same size as
everywhere else; only the OUTSIDE spacing got tighter.
@adewale adewale merged commit a8d73c3 into main May 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants