From 87a4dcf032a4885ed658e6c337c6d7b7d68f9cc3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 22:50:52 +0000 Subject: [PATCH 01/14] Score the journey + example rubrics; shrink hero; animate hero collapse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...1452cc5609f2.css => site.2bb247856b55.css} | 17 +- public/site.css | 17 +- src/asset_manifest.py | 4 +- src/marginalia.py | 161 ++++++++++++++++++ tests/test_marginalia_geometry.py | 51 ++++++ 5 files changed, 244 insertions(+), 6 deletions(-) rename public/{site.1452cc5609f2.css => site.2bb247856b55.css} (93%) diff --git a/public/site.1452cc5609f2.css b/public/site.2bb247856b55.css similarity index 93% rename from public/site.1452cc5609f2.css rename to public/site.2bb247856b55.css index 588fc11..2715d29 100644 --- a/public/site.1452cc5609f2.css +++ b/public/site.2bb247856b55.css @@ -28,8 +28,21 @@ .brand { font-weight: 800; } .nav-links { display: flex; gap: .35rem; } .nav-links a { padding: 0 .9rem; color: var(--muted); } - .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); } - .hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; } + .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; } + .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); } + .hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; } + @supports (animation-timeline: scroll()) { + @media (prefers-reduced-motion: no-preference) { + .hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; } + header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + } + } + @keyframes hero-collapse { + to { transform: scale(0.55) translateY(-32px); opacity: 0; } + } + @keyframes header-solidify { + to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } + } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } diff --git a/public/site.css b/public/site.css index 588fc11..2715d29 100644 --- a/public/site.css +++ b/public/site.css @@ -28,8 +28,21 @@ .brand { font-weight: 800; } .nav-links { display: flex; gap: .35rem; } .nav-links a { padding: 0 .9rem; color: var(--muted); } - .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.5rem, 5vw, 4rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); } - .hero p { max-width: 66ch; color: var(--muted); font-size: 1.08rem; } + .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; } + .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); } + .hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; } + @supports (animation-timeline: scroll()) { + @media (prefers-reduced-motion: no-preference) { + .hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; } + header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + } + } + @keyframes hero-collapse { + to { transform: scale(0.55) translateY(-32px); opacity: 0; } + } + @keyframes header-solidify { + to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } + } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 929c743..1482fb5 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.1452cc5609f2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '2ef350ca9050' +ASSET_PATHS = {'SITE_CSS': '/site.2bb247856b55.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '2bb58e966c67' diff --git a/src/marginalia.py b/src/marginalia.py index bd2305b..0ecea4c 100644 --- a/src/marginalia.py +++ b/src/marginalia.py @@ -2025,6 +2025,46 @@ def render_for_section(section_title: str) -> str: return f'
{_render_svg(name)}{cap}
' +# ─── Section-figure scores ──────────────────────────────────────────── +# Score every journey-section figure against +# docs/journey-visualisation-rubric.md (10-point scale). +# Keyed by section title to match SECTION_FIGURES. +SECTION_FIGURE_SCORES: dict[str, tuple[float, str]] = { + # Runtime + "Start with executable evidence.": (9.0, "program → output, the smallest mechanism"), + "Separate value, identity, and absence.": (9.0, "shared vs separate object identity"), + "Read expressions as object operations.": (9.0, "syntax dispatches to method"), + # Control Flow + "Choose between paths.": (9.0, "value flows through predicate to branches"), + "Name and shape decisions.": (8.5, "walrus name + value; abstract"), + "Stop as soon as the answer is known.": (9.0, "first match; break short-circuits"), + # Iteration + "Choose the right loop shape.": (8.5, "loop body + back-edge; abstract"), + "See the protocol behind `for`.": (9.5, "the canonical iter()/next() picture"), + "Compose lazy value streams.": (9.0, "filter → map; values flow lazily"), + # Workers + "Replace unavailable process boundaries with portable evidence.": (8.0, "constraint figure; no clean mechanism"), + "Keep network lessons local to the protocol boundary.": (8.0, "constraint figure; protocol shape"), + "Preserve the lesson while respecting the runtime.": (8.0, "constraint figure; lesson survives"), + # Shapes + "Pick the container that matches the question.": (9.0, "list/tuple/dict/set per question"), + "Move between shapes deliberately.": (9.0, "input → transform → result"), + "Cross text and data boundaries.": (9.0, "text in, structured value out"), + # Interfaces + "Start with functions as named behavior.": (8.5, "args → body → return; abstract"), + "Use functions as values.": (9.0, "second name binds same function"), + "Bundle behavior with state.": (9.0, "class groups state + methods"), + # Types + "Keep runtime and static analysis separate.": (9.0, "annotations as ghost over signature"), + "Describe realistic data shapes.": (9.0, "x: int|str|None branches"), + "Scale annotations for reusable libraries.": (9.0, "T preserved across the call"), + # Reliability + "Make failure explicit.": (9.0, "try/except/else/finally as lanes"), + "Control resource and module boundaries.": (9.0, "in → body → out with __exit__ dashed"), + "Handle operations that outlive one expression.": (9.0, "loop and coroutine swap on await"), +} + + # ─── Scores (v2 rubric — see docs/example-figure-rubric.md) ──────────── # Score every attached example figure against the v2 rubric. The dict is # the single source of truth for both the gestalt review pages @@ -2150,3 +2190,124 @@ def render_for_section(section_title: str) -> str: def figure_score(slug: str) -> tuple[float, str] | None: """Return the v2 score and rationale for an attached example slug, if any.""" return SCORES.get(slug) + + +# ─── Example quality scores ────────────────────────────────────────── +# Score every example PAGE against docs/example-quality-rubric.md. +# These are HEURISTIC baselines computed from observable structural +# signals (cells with output, see_also density, notes count, +# explanation depth). Manual rubric review can refine any entry; the +# point of the registry is to surface distribution and outliers, not +# to pretend a script can grade pedagogy. + +EXAMPLE_QUALITY_SCORES: dict[str, tuple[float, str]] = { + "hello-world": (7.1, "isolated"), + "values": (8.2, "isolated"), + "literals": (8.8, "graph-rich, note-heavy, multi-cell"), + "numbers": (9.0, "graph-rich, note-heavy"), + "booleans": (8.2, "isolated, note-heavy"), + "operators": (8.8, "graph-rich, note-heavy, multi-cell"), + "none": (8.2, "isolated"), + "variables": (8.2, "isolated"), + "constants": (7.7, "isolated"), + "truthiness": (7.9, "isolated"), + "equality-and-identity": (8.4, "isolated, note-heavy"), + "mutability": (8.2, "isolated"), + "object-lifecycle": (7.4, "isolated"), + "strings": (8.2, "isolated, note-heavy"), + "bytes-and-bytearray": (9.0, "graph-rich, note-heavy"), + "string-formatting": (8.2, "isolated"), + "conditionals": (8.2, "isolated"), + "guard-clauses": (7.4, "isolated"), + "assignment-expressions": (8.5, "graph-rich"), + "for-loops": (7.3, "isolated"), + "break-and-continue": (8.5, "graph-rich"), + "loop-else": (8.5, "graph-rich"), + "iterating-over-iterables": (8.8, "graph-rich"), + "iterators": (8.8, "graph-rich"), + "iterator-vs-iterable": (9.0, "graph-rich"), + "sentinel-iteration": (7.4, "isolated"), + "match-statements": (8.2, "isolated"), + "advanced-match-patterns": (8.8, "graph-rich"), + "while-loops": (8.0, "isolated"), + "lists": (8.2, "isolated"), + "tuples": (9.0, "graph-rich, note-heavy"), + "unpacking": (8.2, "isolated"), + "dicts": (8.4, "isolated, note-heavy"), + "sets": (8.2, "isolated, note-heavy"), + "slices": (8.0, "isolated"), + "comprehensions": (8.2, "isolated, note-heavy"), + "comprehension-patterns": (8.5, "graph-rich"), + "sorting": (8.2, "isolated"), + "collections-module": (7.4, "isolated"), + "copying-collections": (7.4, "isolated"), + "functions": (8.4, "isolated, note-heavy"), + "keyword-only-arguments": (8.2, "isolated"), + "positional-only-parameters": (8.5, "graph-rich"), + "args-and-kwargs": (8.2, "isolated"), + "multiple-return-values": (8.0, "isolated"), + "closures": (8.2, "isolated, note-heavy"), + "partial-functions": (7.4, "isolated"), + "scope-global-nonlocal": (8.5, "graph-rich"), + "recursion": (8.0, "isolated"), + "lambdas": (8.2, "isolated"), + "generators": (9.0, "graph-rich, note-heavy"), + "yield-from": (8.5, "graph-rich"), + "generator-expressions": (8.2, "isolated"), + "itertools": (8.2, "isolated, note-heavy"), + "decorators": (8.8, "graph-rich"), + "classes": (9.0, "graph-rich, note-heavy"), + "inheritance-and-super": (8.5, "graph-rich"), + "classmethods-and-staticmethods": (9.0, "graph-rich, note-heavy"), + "dataclasses": (8.8, "graph-rich"), + "properties": (8.2, "isolated"), + "special-methods": (8.5, "graph-rich, note-heavy, multi-cell"), + "truth-and-size": (8.8, "graph-rich"), + "container-protocols": (8.8, "graph-rich"), + "callable-objects": (8.8, "graph-rich"), + "operator-overloading": (8.8, "graph-rich"), + "attribute-access": (8.8, "graph-rich"), + "bound-and-unbound-methods": (9.0, "graph-rich, note-heavy"), + "descriptors": (8.0, "graph-rich"), + "metaclasses": (8.5, "graph-rich"), + "context-managers": (8.8, "graph-rich, note-heavy"), + "delete-statements": (8.8, "graph-rich"), + "exceptions": (8.2, "isolated, note-heavy"), + "assertions": (8.5, "graph-rich"), + "exception-chaining": (8.5, "graph-rich"), + "exception-groups": (8.5, "graph-rich"), + "warnings": (7.4, "isolated"), + "modules": (9.0, "graph-rich, note-heavy"), + "import-aliases": (8.5, "graph-rich, note-heavy"), + "packages": (9.0, "graph-rich, note-heavy"), + "virtual-environments": (7.7, "isolated"), + "type-hints": (8.8, "graph-rich, note-heavy, multi-cell"), + "runtime-type-checks": (8.8, "graph-rich"), + "union-and-optional-types": (8.8, "graph-rich"), + "type-aliases": (8.8, "graph-rich"), + "typed-dicts": (8.8, "graph-rich"), + "structured-data-shapes": (9.0, "graph-rich, note-heavy"), + "literal-and-final": (7.4, "isolated"), + "callable-types": (8.8, "graph-rich"), + "generics-and-typevar": (8.8, "graph-rich"), + "paramspec": (7.4, "isolated"), + "overloads": (7.4, "isolated"), + "casts-and-any": (8.8, "graph-rich"), + "newtype": (8.8, "graph-rich"), + "protocols": (8.8, "graph-rich"), + "abstract-base-classes": (9.0, "graph-rich, note-heavy"), + "enums": (8.0, "isolated, note-heavy"), + "regular-expressions": (8.8, "graph-rich, note-heavy, multi-cell"), + "number-parsing": (7.7, "isolated"), + "custom-exceptions": (8.2, "isolated"), + "json": (9.0, "graph-rich, note-heavy"), + "logging": (7.4, "isolated"), + "testing": (8.8, "graph-rich"), + "subprocesses": (7.7, "isolated"), + "threads-and-processes": (7.9, "isolated"), + "networking": (7.7, "isolated"), + "datetime": (8.2, "isolated"), + "csv-data": (7.4, "isolated"), + "async-await": (9.0, "graph-rich, note-heavy"), + "async-iteration-and-context": (8.8, "graph-rich"), +} diff --git a/tests/test_marginalia_geometry.py b/tests/test_marginalia_geometry.py index 7ce25e9..34b3279 100644 --- a/tests/test_marginalia_geometry.py +++ b/tests/test_marginalia_geometry.py @@ -394,6 +394,57 @@ def test_every_section_figure_caption_is_unique(self): self.assertEqual(duplicates, {}, f"duplicate section captions: {duplicates}") + def test_every_section_has_a_score(self): + from src.marginalia import SECTION_FIGURES, SECTION_FIGURE_SCORES + + unscored = set(SECTION_FIGURES) - set(SECTION_FIGURE_SCORES) + unattached = set(SECTION_FIGURE_SCORES) - set(SECTION_FIGURES) + self.assertEqual(unscored, set(), f"unscored sections: {sorted(unscored)}") + self.assertEqual(unattached, set(), f"scored but unattached: {sorted(unattached)}") + + def test_every_section_score_in_range(self): + from src.marginalia import SECTION_FIGURE_SCORES + + failures: list[str] = [] + for title, entry in SECTION_FIGURE_SCORES.items(): + if not isinstance(entry, tuple) or len(entry) != 2: + failures.append(f"{title!r}: not a (score, commentary) tuple") + continue + score, commentary = entry + if not isinstance(score, (int, float)) or not 0 <= score <= 10: + failures.append(f"{title!r}: score {score!r} outside [0, 10]") + if not isinstance(commentary, str) or not commentary.strip(): + failures.append(f"{title!r}: empty commentary") + self.assertEqual(failures, [], "\n " + "\n ".join(failures)) + + + def test_every_example_has_a_quality_score(self): + from src.example_loader import load_examples + from src.marginalia import EXAMPLE_QUALITY_SCORES + + _, examples = load_examples() + slugs = {ex["slug"] for ex in examples} + unscored = slugs - set(EXAMPLE_QUALITY_SCORES) + ghost = set(EXAMPLE_QUALITY_SCORES) - slugs + self.assertEqual(unscored, set(), f"unscored examples: {sorted(unscored)}") + self.assertEqual(ghost, set(), f"scored but no example: {sorted(ghost)}") + + def test_every_example_quality_score_in_range(self): + from src.marginalia import EXAMPLE_QUALITY_SCORES + + failures: list[str] = [] + for slug, entry in EXAMPLE_QUALITY_SCORES.items(): + if not isinstance(entry, tuple) or len(entry) != 2: + failures.append(f"{slug}: not a tuple") + continue + score, commentary = entry + if not isinstance(score, (int, float)) or not 0 <= score <= 10: + failures.append(f"{slug}: score {score!r} outside [0, 10]") + if not isinstance(commentary, str) or not commentary.strip(): + failures.append(f"{slug}: empty commentary") + self.assertEqual(failures, [], "\n " + "\n ".join(failures)) + + class FigureCaptionContract(unittest.TestCase): """Contract 5b: every attachment caption is unique. From c637590e087829256abb32971d2f202b827cc3f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 22:57:48 +0000 Subject: [PATCH 02/14] Hero h1 morphs into the top-left wordmark on scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/ and /journeys/ 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. --- ...2bb247856b55.css => site.b7913985255d.css} | 22 ++++++++++++++----- public/site.css | 22 ++++++++++++++----- src/asset_manifest.py | 4 ++-- 3 files changed, 36 insertions(+), 12 deletions(-) rename public/{site.2bb247856b55.css => site.b7913985255d.css} (93%) diff --git a/public/site.2bb247856b55.css b/public/site.b7913985255d.css similarity index 93% rename from public/site.2bb247856b55.css rename to public/site.b7913985255d.css index 2715d29..7856afb 100644 --- a/public/site.2bb247856b55.css +++ b/public/site.b7913985255d.css @@ -28,17 +28,29 @@ .brand { font-weight: 800; } .nav-links { display: flex; gap: .35rem; } .nav-links a { padding: 0 .9rem; color: var(--muted); } - .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; } - .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); } + .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top left; } + .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); transform-origin: top left; } .hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; } @supports (animation-timeline: scroll()) { @media (prefers-reduced-motion: no-preference) { - .hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; } + .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } + .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } + body:has(.hero) header .brand { opacity: 0; filter: blur(4px); transform: scale(0.88); animation: brand-reveal linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } } } - @keyframes hero-collapse { - to { transform: scale(0.55) translateY(-32px); opacity: 0; } + @keyframes hero-fade { + to { opacity: 0; transform: scale(0.92) translateY(-8px); } + } + @keyframes hero-h1-morph { + to { transform: scale(0.32) translate(-32%, -50%); opacity: 0; } + } + @keyframes hero-p-fade { + to { opacity: 0; transform: translateY(-4px); } + } + @keyframes brand-reveal { + to { opacity: 1; filter: blur(0); transform: scale(1); } } @keyframes header-solidify { to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } diff --git a/public/site.css b/public/site.css index 2715d29..7856afb 100644 --- a/public/site.css +++ b/public/site.css @@ -28,17 +28,29 @@ .brand { font-weight: 800; } .nav-links { display: flex; gap: .35rem; } .nav-links a { padding: 0 .9rem; color: var(--muted); } - .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top center; } - .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); } + .hero { overflow: hidden; border: 1px solid var(--hairline); border-radius: 1rem; padding: clamp(1.25rem, 3.5vw, 2.5rem); margin-bottom: 1.25rem; background: linear-gradient(135deg, var(--surface), var(--surface-3)); box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transform-origin: top left; } + .hero h1 { font-size: clamp(2rem, 4vw, 3rem); margin-bottom: var(--space-3); transform-origin: top left; } .hero p { max-width: 60ch; color: var(--muted); font-size: 1rem; } @supports (animation-timeline: scroll()) { @media (prefers-reduced-motion: no-preference) { - .hero { animation: hero-collapse linear forwards; animation-timeline: scroll(root); animation-range: 0 320px; } + .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } + .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } + body:has(.hero) header .brand { opacity: 0; filter: blur(4px); transform: scale(0.88); animation: brand-reveal linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } } } - @keyframes hero-collapse { - to { transform: scale(0.55) translateY(-32px); opacity: 0; } + @keyframes hero-fade { + to { opacity: 0; transform: scale(0.92) translateY(-8px); } + } + @keyframes hero-h1-morph { + to { transform: scale(0.32) translate(-32%, -50%); opacity: 0; } + } + @keyframes hero-p-fade { + to { opacity: 0; transform: translateY(-4px); } + } + @keyframes brand-reveal { + to { opacity: 1; filter: blur(0); transform: scale(1); } } @keyframes header-solidify { to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 1482fb5..0433e6a 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.2bb247856b55.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '2bb58e966c67' +ASSET_PATHS = {'SITE_CSS': '/site.b7913985255d.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '801d4d11e527' From 45a59c7a9776983b088fbece488eacf934e2e231 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:02:44 +0000 Subject: [PATCH 03/14] Header starts invisible on home; emerges as the hero scrolls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../{site.b7913985255d.css => site.b5d5cf89f344.css} | 12 ++++++------ public/site.css | 12 ++++++------ src/asset_manifest.py | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) rename public/{site.b7913985255d.css => site.b5d5cf89f344.css} (96%) diff --git a/public/site.b7913985255d.css b/public/site.b5d5cf89f344.css similarity index 96% rename from public/site.b7913985255d.css rename to public/site.b5d5cf89f344.css index 7856afb..6c26d33 100644 --- a/public/site.b7913985255d.css +++ b/public/site.b5d5cf89f344.css @@ -36,8 +36,8 @@ .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } - body:has(.hero) header .brand { opacity: 0; filter: blur(4px); transform: scale(0.88); animation: brand-reveal linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } - header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } + body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } } } @keyframes hero-fade { @@ -49,11 +49,11 @@ @keyframes hero-p-fade { to { opacity: 0; transform: translateY(-4px); } } - @keyframes brand-reveal { - to { opacity: 1; filter: blur(0); transform: scale(1); } + @keyframes brand-focus { + to { filter: blur(0); transform: scale(1); } } - @keyframes header-solidify { - to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } + @keyframes header-emerge { + to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } diff --git a/public/site.css b/public/site.css index 7856afb..6c26d33 100644 --- a/public/site.css +++ b/public/site.css @@ -36,8 +36,8 @@ .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } - body:has(.hero) header .brand { opacity: 0; filter: blur(4px); transform: scale(0.88); animation: brand-reveal linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } - header { animation: header-solidify linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } + body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } + body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } } } @keyframes hero-fade { @@ -49,11 +49,11 @@ @keyframes hero-p-fade { to { opacity: 0; transform: translateY(-4px); } } - @keyframes brand-reveal { - to { opacity: 1; filter: blur(0); transform: scale(1); } + @keyframes brand-focus { + to { filter: blur(0); transform: scale(1); } } - @keyframes header-solidify { - to { background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } + @keyframes header-emerge { + to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 0433e6a..bea5c0d 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.b7913985255d.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '801d4d11e527' +ASSET_PATHS = {'SITE_CSS': '/site.b5d5cf89f344.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '0476ea74e5b1' From 8508fe7810be040a740ce2ff0e6e88a25739acc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:05:56 +0000 Subject: [PATCH 04/14] =?UTF-8?q?Nav:=20header=20=E2=86=92=20Journeys;=20f?= =?UTF-8?q?ooter=20=E2=86=92=20Python=20docs=20link;=20remove=20Start?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/asset_manifest.py | 2 +- src/templates/layout.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/asset_manifest.py b/src/asset_manifest.py index bea5c0d..4c73f74 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. ASSET_PATHS = {'SITE_CSS': '/site.b5d5cf89f344.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '0476ea74e5b1' +HTML_CACHE_VERSION = '1718c453ecb2' diff --git a/src/templates/layout.html b/src/templates/layout.html index dadc319..883757a 100644 --- a/src/templates/layout.html +++ b/src/templates/layout.html @@ -20,8 +20,9 @@
- +
__CONTENT__
+ From 5ffb3bb35b877c914390cf738eabcfd9c3cde4c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:09:03 +0000 Subject: [PATCH 05/14] journeys index: link 'Apprenticeship Patterns' to the O'Reilly book 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. --- src/app.py | 2 +- src/asset_manifest.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app.py b/src/app.py index e11cc60..9c79b83 100644 --- a/src/app.py +++ b/src/app.py @@ -542,7 +542,7 @@ def render_journeys_index():

Journeys

Python learning journeys

-

These paths compose individual examples into larger mental maps. They are inspired by the way Apprenticeship Patterns treats small patterns as material for longer learning journeys.

+

These paths compose individual examples into larger mental maps. They are inspired by the way Apprenticeship Patterns treats small patterns as material for longer learning journeys.

{"".join(cards)}
''' diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 4c73f74..bbec311 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. ASSET_PATHS = {'SITE_CSS': '/site.b5d5cf89f344.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '1718c453ecb2' +HTML_CACHE_VERSION = 'a7b91f426795' From 2c03a52cb8c2ecc923aa166631e52e6f5645a20d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:12:57 +0000 Subject: [PATCH 06/14] =?UTF-8?q?"All=20examples"=20back-link=20uses=20?= =?UTF-8?q?=E2=86=91=20(up=20arrow)=20instead=20of=20=E2=86=90=20(left=20a?= =?UTF-8?q?rrow)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- scripts/build_prototypes.py | 2 +- src/app.py | 2 +- src/asset_manifest.py | 2 +- src/templates/example.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build_prototypes.py b/scripts/build_prototypes.py index 2406792..e85c508 100644 --- a/scripts/build_prototypes.py +++ b/scripts/build_prototypes.py @@ -102,7 +102,7 @@ def render_article(example: dict, *, banners: dict[str, str] | None = None) -> s output = html.escape(example.get("expected_output", "")) return f"""
- +

{html.escape(example['section'])}

{html.escape(example['title'])}

diff --git a/src/app.py b/src/app.py index 9c79b83..3d1b5ca 100644 --- a/src/app.py +++ b/src/app.py @@ -576,7 +576,7 @@ def render_journey_page(journey): sections.append(f'

{html.escape(section["title"])}

{html.escape(section["summary"])}

{figure_html}
    {"".join(rows)}
') content = f'''
- +

Journey

{html.escape(journey["title"])}

diff --git a/src/asset_manifest.py b/src/asset_manifest.py index bbec311..dc1b9a9 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. ASSET_PATHS = {'SITE_CSS': '/site.b5d5cf89f344.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = 'a7b91f426795' +HTML_CACHE_VERSION = 'b61de6981e3d' diff --git a/src/templates/example.html b/src/templates/example.html index aa50583..79c1fc9 100644 --- a/src/templates/example.html +++ b/src/templates/example.html @@ -1,5 +1,5 @@
- +

__SECTION__

__TITLE__

From 4002c27bdf4267b158aeba627520738da4844db1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:18:43 +0000 Subject: [PATCH 07/14] =?UTF-8?q?Playground=20panels=20stack=20earlier=20?= =?UTF-8?q?=E2=80=94=20runner-grid=20collapses=20at=20980px?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/site.css | 1 + public/{site.b5d5cf89f344.css => site.ec7d95a7e8bc.css} | 1 + src/asset_manifest.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) rename public/{site.b5d5cf89f344.css => site.ec7d95a7e8bc.css} (99%) diff --git a/public/site.css b/public/site.css index 6c26d33..202a7b5 100644 --- a/public/site.css +++ b/public/site.css @@ -106,6 +106,7 @@ .playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } .playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); } .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } + @media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } diff --git a/public/site.b5d5cf89f344.css b/public/site.ec7d95a7e8bc.css similarity index 99% rename from public/site.b5d5cf89f344.css rename to public/site.ec7d95a7e8bc.css index 6c26d33..202a7b5 100644 --- a/public/site.b5d5cf89f344.css +++ b/public/site.ec7d95a7e8bc.css @@ -106,6 +106,7 @@ .playground { margin-top: var(--space-6); padding-top: var(--space-4); border-top: 1px solid var(--hairline); } .playground > h2 { font-size: clamp(1.5rem, 2.5vw, 2rem); letter-spacing: -0.03em; margin-bottom: var(--space-3); } .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } + @media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index dc1b9a9..5128532 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.b5d5cf89f344.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = 'b61de6981e3d' +ASSET_PATHS = {'SITE_CSS': '/site.ec7d95a7e8bc.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '6e47ba57c311' From 281a1a36a53069f3d09b36486704360a4a3e8df0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:22:02 +0000 Subject: [PATCH 08/14] Make the Example code panel read as editable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/{site.ec7d95a7e8bc.css => site.0e32b85ff236.css} | 4 ++++ public/site.css | 4 ++++ src/asset_manifest.py | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) rename public/{site.ec7d95a7e8bc.css => site.0e32b85ff236.css} (96%) diff --git a/public/site.ec7d95a7e8bc.css b/public/site.0e32b85ff236.css similarity index 96% rename from public/site.ec7d95a7e8bc.css rename to public/site.0e32b85ff236.css index 202a7b5..ea771dc 100644 --- a/public/site.ec7d95a7e8bc.css +++ b/public/site.0e32b85ff236.css @@ -110,6 +110,10 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } + .runner-editor { border-style: solid; border-color: var(--hairline); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:hover { border-color: var(--accent); } + .runner-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } + .runner-editor h2::before { content: "✎ "; color: var(--accent); font-weight: 400; } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/public/site.css b/public/site.css index 202a7b5..ea771dc 100644 --- a/public/site.css +++ b/public/site.css @@ -110,6 +110,10 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } + .runner-editor { border-style: solid; border-color: var(--hairline); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:hover { border-color: var(--accent); } + .runner-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } + .runner-editor h2::before { content: "✎ "; color: var(--accent); font-weight: 400; } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 5128532..3e8b188 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.ec7d95a7e8bc.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '6e47ba57c311' +ASSET_PATHS = {'SITE_CSS': '/site.0e32b85ff236.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '11cb4a87b617' From d5944549967efbd0cb4a41356d7b29bfec764c1f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:25:43 +0000 Subject: [PATCH 09/14] Polish the editor affordance with the existing accent left-rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/{site.0e32b85ff236.css => site.16ea7dd6d9b3.css} | 7 +++---- public/site.css | 7 +++---- src/asset_manifest.py | 4 ++-- 3 files changed, 8 insertions(+), 10 deletions(-) rename public/{site.0e32b85ff236.css => site.16ea7dd6d9b3.css} (96%) diff --git a/public/site.0e32b85ff236.css b/public/site.16ea7dd6d9b3.css similarity index 96% rename from public/site.0e32b85ff236.css rename to public/site.16ea7dd6d9b3.css index ea771dc..90cda6d 100644 --- a/public/site.0e32b85ff236.css +++ b/public/site.16ea7dd6d9b3.css @@ -110,10 +110,9 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border-style: solid; border-color: var(--hairline); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:hover { border-color: var(--accent); } - .runner-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } - .runner-editor h2::before { content: "✎ "; color: var(--accent); font-weight: 400; } + .runner-editor { border: 1px solid var(--hairline); border-left: 2px solid var(--accent); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow, background-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:hover { background: var(--surface-3); } + .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/public/site.css b/public/site.css index ea771dc..90cda6d 100644 --- a/public/site.css +++ b/public/site.css @@ -110,10 +110,9 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border-style: solid; border-color: var(--hairline); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:hover { border-color: var(--accent); } - .runner-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } - .runner-editor h2::before { content: "✎ "; color: var(--accent); font-weight: 400; } + .runner-editor { border: 1px solid var(--hairline); border-left: 2px solid var(--accent); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow, background-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:hover { background: var(--surface-3); } + .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 3e8b188..8431e07 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.0e32b85ff236.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '11cb4a87b617' +ASSET_PATHS = {'SITE_CSS': '/site.16ea7dd6d9b3.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = 'af940b56156d' From 664205daf6eceadb86641e3c9bf9b85d62953061 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:28:21 +0000 Subject: [PATCH 10/14] Remove the asymmetric accent left rule on the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/{site.16ea7dd6d9b3.css => site.ad38e31469d2.css} | 3 +-- public/site.css | 3 +-- src/asset_manifest.py | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) rename public/{site.16ea7dd6d9b3.css => site.ad38e31469d2.css} (97%) diff --git a/public/site.16ea7dd6d9b3.css b/public/site.ad38e31469d2.css similarity index 97% rename from public/site.16ea7dd6d9b3.css rename to public/site.ad38e31469d2.css index 90cda6d..528eb5a 100644 --- a/public/site.16ea7dd6d9b3.css +++ b/public/site.ad38e31469d2.css @@ -110,8 +110,7 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border: 1px solid var(--hairline); border-left: 2px solid var(--accent); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow, background-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:hover { background: var(--surface-3); } + .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } diff --git a/public/site.css b/public/site.css index 90cda6d..528eb5a 100644 --- a/public/site.css +++ b/public/site.css @@ -110,8 +110,7 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border: 1px solid var(--hairline); border-left: 2px solid var(--accent); background: var(--surface-2); cursor: text; transition-property: border-color, box-shadow, background-color; transition-duration: 160ms; transition-timing-function: cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:hover { background: var(--surface-3); } + .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 8431e07..748b632 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.16ea7dd6d9b3.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = 'af940b56156d' +ASSET_PATHS = {'SITE_CSS': '/site.ad38e31469d2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = '01251bd4beba' From 20d4e6f5355594d5d8214db18210453f93b7f86b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:33:59 +0000 Subject: [PATCH 11/14] =?UTF-8?q?Playground=20sub-headings:=20h2=20?= =?UTF-8?q?=E2=86=92=20h3=20to=20nest=20under=20the=20section=20h2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- public/{site.ad38e31469d2.css => site.9ad0aa5c4ab6.css} | 2 +- public/site.css | 2 +- scripts/build_prototypes.py | 4 ++-- src/asset_manifest.py | 4 ++-- src/templates/example.html | 4 ++-- tests/test_app.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) rename public/{site.ad38e31469d2.css => site.9ad0aa5c4ab6.css} (99%) diff --git a/public/site.ad38e31469d2.css b/public/site.9ad0aa5c4ab6.css similarity index 99% rename from public/site.ad38e31469d2.css rename to public/site.9ad0aa5c4ab6.css index 528eb5a..33dc88d 100644 --- a/public/site.ad38e31469d2.css +++ b/public/site.9ad0aa5c4ab6.css @@ -108,7 +108,7 @@ .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } @media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } - .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } + .runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } diff --git a/public/site.css b/public/site.css index 528eb5a..33dc88d 100644 --- a/public/site.css +++ b/public/site.css @@ -108,7 +108,7 @@ .runner-grid { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(18rem, .75fr); gap: var(--space-4); align-items: stretch; } @media (max-width: 980px) { .runner-grid { grid-template-columns: 1fr; } } .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } - .runner-panel h2 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } + .runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } diff --git a/scripts/build_prototypes.py b/scripts/build_prototypes.py index e85c508..76b689d 100644 --- a/scripts/build_prototypes.py +++ b/scripts/build_prototypes.py @@ -115,10 +115,10 @@ def render_article(example: dict, *, banners: dict[str, str] | None = None) -> s

Run the complete example

-

Example code

+

Example code

{code}
-

Expected output

{output}
+

Expected output

{output}
diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 748b632..c5e0890 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.ad38e31469d2.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = '01251bd4beba' +ASSET_PATHS = {'SITE_CSS': '/site.9ad0aa5c4ab6.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} +HTML_CACHE_VERSION = 'eaa23cebb468' diff --git a/src/templates/example.html b/src/templates/example.html index 79c1fc9..2d1ae28 100644 --- a/src/templates/example.html +++ b/src/templates/example.html @@ -14,14 +14,14 @@

__TITLE__

Run the complete example

-

Example code

+

Example code

-

__OUTPUT_HEADING__

__SHOWN_OUTPUT__

__EXECUTION_TIME__

+

__OUTPUT_HEADING__

__SHOWN_OUTPUT__

__EXECUTION_TIME__

diff --git a/tests/test_app.py b/tests/test_app.py index 25c4a1a..cc1c81b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -321,7 +321,7 @@ def test_cf_workers_design_system_and_playground_lessons(self): self.assertNotIn('data-share', html) self.assertIn('output-panel', html) self.assertIn(".runner-panel", css) - self.assertIn(".runner-panel h2", css) + self.assertIn(".runner-panel h3", css) self.assertIn("text-underline-offset", css) self.assertIn('aria-live="polite"', html) self.assertIn('min-height: 18rem', css) From 92691aaf79f16b4e478e69e6be8ed18f2d50f1e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 11 May 2026 23:54:49 +0000 Subject: [PATCH 12/14] Editor: inset shadow + line-number gutter; home: section eyebrows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 opening tag. 62 tests pass; SEO/cache lint clears 110 pages; ruff clean. --- ...dd81f5171b14.js => editor.a4a7766e1b9b.js} | 3 +- public/editor.js | 3 +- public/site.css | 9 ++++-- ...9ad0aa5c4ab6.css => site.f3fd8c78f5ba.css} | 9 ++++-- src/app.py | 29 ++++++++++++------- src/asset_manifest.py | 4 +-- 6 files changed, 36 insertions(+), 21 deletions(-) rename public/{editor.dd81f5171b14.js => editor.a4a7766e1b9b.js} (91%) rename public/{site.9ad0aa5c4ab6.css => site.f3fd8c78f5ba.css} (96%) diff --git a/public/editor.dd81f5171b14.js b/public/editor.a4a7766e1b9b.js similarity index 91% rename from public/editor.dd81f5171b14.js rename to public/editor.a4a7766e1b9b.js index 646b689..5db42ec 100644 --- a/public/editor.dd81f5171b14.js +++ b/public/editor.a4a7766e1b9b.js @@ -1,5 +1,5 @@ import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; -import { EditorView } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; +import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1'; import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; @@ -15,6 +15,7 @@ if (textarea && form) { extensions: [ python(), syntaxHighlighting(defaultHighlightStyle), + lineNumbers(), EditorView.lineWrapping, EditorView.updateListener.of((update) => { if (update.docChanged) textarea.value = update.state.doc.toString(); diff --git a/public/editor.js b/public/editor.js index 646b689..5db42ec 100644 --- a/public/editor.js +++ b/public/editor.js @@ -1,5 +1,5 @@ import { EditorState } from 'https://esm.sh/@codemirror/state@6.5.2'; -import { EditorView } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; +import { EditorView, lineNumbers } from 'https://esm.sh/@codemirror/view@6.41.1?deps=@codemirror/state@6.5.2'; import { defaultHighlightStyle, syntaxHighlighting } from 'https://esm.sh/@codemirror/language@6.12.3?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1'; import { python } from 'https://esm.sh/@codemirror/lang-python@6.2.1?deps=@codemirror/state@6.5.2,@codemirror/view@6.41.1,@codemirror/language@6.12.3'; @@ -15,6 +15,7 @@ if (textarea && form) { extensions: [ python(), syntaxHighlighting(defaultHighlightStyle), + lineNumbers(), EditorView.lineWrapping, EditorView.updateListener.of((update) => { if (update.docChanged) textarea.value = update.state.doc.toString(); diff --git a/public/site.css b/public/site.css index 33dc88d..7b7f0e6 100644 --- a/public/site.css +++ b/public/site.css @@ -20,7 +20,8 @@ .cm-scroller { font-family: inherit; line-height: 1.5; } .cm-content { padding: 0; } .cm-line { padding: 0; } - .cm-gutters { display: none; } + .cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; } + .cm-lineNumbers .cm-gutterElement { padding: 0 var(--space-2) 0 0; min-width: 2ch; text-align: right; font-size: .85em; } .cm-activeLine, .cm-activeLineGutter { background: transparent; } .cm-selectionBackground, .cm-focused .cm-selectionBackground { background: rgba(255, 72, 1, 0.18) !important; } code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; } @@ -56,6 +57,8 @@ to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } + .grid-section { grid-column: 1 / -1; margin: var(--space-4) 0 0; } + .grid-section:first-child { margin-top: 0; } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } @@ -110,8 +113,8 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } + .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04); transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:focus-within { box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04), 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/public/site.9ad0aa5c4ab6.css b/public/site.f3fd8c78f5ba.css similarity index 96% rename from public/site.9ad0aa5c4ab6.css rename to public/site.f3fd8c78f5ba.css index 33dc88d..7b7f0e6 100644 --- a/public/site.9ad0aa5c4ab6.css +++ b/public/site.f3fd8c78f5ba.css @@ -20,7 +20,8 @@ .cm-scroller { font-family: inherit; line-height: 1.5; } .cm-content { padding: 0; } .cm-line { padding: 0; } - .cm-gutters { display: none; } + .cm-gutters { background: transparent; border-right: 1px solid var(--hairline-soft); color: var(--muted); font-variant-numeric: tabular-nums; } + .cm-lineNumbers .cm-gutterElement { padding: 0 var(--space-2) 0 0; min-width: 2ch; text-align: right; font-size: .85em; } .cm-activeLine, .cm-activeLineGutter { background: transparent; } .cm-selectionBackground, .cm-focused .cm-selectionBackground { background: rgba(255, 72, 1, 0.18) !important; } code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; } @@ -56,6 +57,8 @@ to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } + .grid-section { grid-column: 1 / -1; margin: var(--space-4) 0 0; } + .grid-section:first-child { margin-top: 0; } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } @@ -110,8 +113,8 @@ .runner-panel { min-height: 18rem; display: flex; flex-direction: column; border: 1px dashed var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface); } .runner-panel h3 { margin: 0 0 var(--space-3); padding-bottom: var(--space-2); border-bottom: 1px solid var(--hairline-soft); font-size: 1.05rem; letter-spacing: -0.02em; } .runner-panel pre { flex: 1; min-height: 0; overflow: visible; white-space: pre-wrap; overflow-wrap: anywhere; margin: 0; } - .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } - .runner-editor:focus-within { box-shadow: 0 0 0 3px rgba(255, 72, 1, 0.12); } + .runner-editor { border-style: solid; background: var(--surface-2); cursor: text; box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04); transition: box-shadow 160ms cubic-bezier(0.2, 0, 0, 1); } + .runner-editor:focus-within { box-shadow: inset 0 1px 2px rgba(82, 16, 0, 0.04), 0 0 0 3px rgba(255, 72, 1, 0.12); } .execution-time { min-height: 1.5rem; margin: var(--space-2) 0 0; padding-top: var(--space-2); border-top: 1px solid var(--hairline-soft); color: var(--muted); font-size: .88rem; font-variant-numeric: tabular-nums; } .playground-toolbar { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin: .8rem 0 1rem; } .tool-button { min-height: 40px; border: 1px solid var(--hairline); border-radius: 9999px; padding: .62rem .9rem; background: var(--surface-2); color: var(--text); cursor: pointer; transition-property: transform, background-color, border-style; transition-duration: 150ms; } diff --git a/src/app.py b/src/app.py index 3d1b5ca..bf38afd 100644 --- a/src/app.py +++ b/src/app.py @@ -499,19 +499,26 @@ def _layout(title: str, content: str, description: str | None = None, path: str def render_home() -> str: - cards = [] + # Group examples by section in the order each section first appears + # in the manifest, then emit one eyebrow divider per section followed + # by every example in that section. + by_section: dict[str, list[dict]] = {} for example in list_examples(): - cards.append( - _replace( - '

__SECTION__

__TITLE__

__SUMMARY__

', - { - "SECTION": html.escape(example["section"]), - "SLUG": html.escape(example["slug"]), - "TITLE": html.escape(example["title"]), - "SUMMARY": html.escape(example["summary"]), - }, + by_section.setdefault(example["section"], []).append(example) + cards = [] + for section, examples in by_section.items(): + cards.append(f'

{html.escape(section)}

') + for example in examples: + cards.append( + _replace( + '

__TITLE__

__SUMMARY__

', + { + "SLUG": html.escape(example["slug"]), + "TITLE": html.escape(example["title"]), + "SUMMARY": html.escape(example["summary"]), + }, + ) ) - ) content = _replace( _template("home.html"), {"PYTHON_VERSION": html.escape(PYTHON_VERSION), "CARDS": "".join(cards)}, diff --git a/src/asset_manifest.py b/src/asset_manifest.py index c5e0890..1758355 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.9ad0aa5c4ab6.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'} -HTML_CACHE_VERSION = 'eaa23cebb468' +ASSET_PATHS = {'SITE_CSS': '/site.f3fd8c78f5ba.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} +HTML_CACHE_VERSION = '6e21b65d3855' From 900088068351f29e160fb03e171624133410e33d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 00:19:28 +0000 Subject: [PATCH 13/14] Section eyebrows: tight to their cards, generous from the section above MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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:

Basics

…cards…

Data Model

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. --- ...f3fd8c78f5ba.css => site.8f693d8043fe.css} | 5 ++- public/site.css | 5 ++- src/app.py | 39 ++++++++++++------- src/asset_manifest.py | 4 +- src/templates/home.html | 2 +- 5 files changed, 33 insertions(+), 22 deletions(-) rename public/{site.f3fd8c78f5ba.css => site.8f693d8043fe.css} (99%) diff --git a/public/site.f3fd8c78f5ba.css b/public/site.8f693d8043fe.css similarity index 99% rename from public/site.f3fd8c78f5ba.css rename to public/site.8f693d8043fe.css index 7b7f0e6..4ccc4db 100644 --- a/public/site.f3fd8c78f5ba.css +++ b/public/site.8f693d8043fe.css @@ -57,8 +57,9 @@ to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } - .grid-section { grid-column: 1 / -1; margin: var(--space-4) 0 0; } - .grid-section:first-child { margin-top: 0; } + .home-section { margin-top: var(--space-6); } + .home-section:first-of-type { margin-top: 0; } + .home-section .eyebrow { margin: 0 0 var(--space-2); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } diff --git a/public/site.css b/public/site.css index 7b7f0e6..4ccc4db 100644 --- a/public/site.css +++ b/public/site.css @@ -57,8 +57,9 @@ to { opacity: 1; background: rgba(245, 241, 235, 0.95); box-shadow: 0 1px 8px rgba(82, 16, 0, 0.06); } } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: var(--space-3); } - .grid-section { grid-column: 1 / -1; margin: var(--space-4) 0 0; } - .grid-section:first-child { margin-top: 0; } + .home-section { margin-top: var(--space-6); } + .home-section:first-of-type { margin-top: 0; } + .home-section .eyebrow { margin: 0 0 var(--space-2); } .card { display: block; min-height: 10rem; border: 1px solid var(--hairline); border-radius: .75rem; padding: var(--space-3); background: var(--surface-2); color: inherit; text-decoration: none; box-shadow: 0 1px 3px rgba(82, 16, 0, 0.04), 0 4px 12px rgba(82, 16, 0, 0.02); transition-property: transform, background-color, border-color; transition-duration: 200ms; transition-timing-function: cubic-bezier(0, 0, 0.2, 1); } .card:hover { transform: translateY(-2px); background: var(--surface-3); border-color: var(--accent); } .card h2 { text-decoration: underline; text-decoration-color: var(--hairline); text-underline-offset: .18em; } diff --git a/src/app.py b/src/app.py index bf38afd..d605513 100644 --- a/src/app.py +++ b/src/app.py @@ -500,28 +500,37 @@ def _layout(title: str, content: str, description: str | None = None, path: str def render_home() -> str: # Group examples by section in the order each section first appears - # in the manifest, then emit one eyebrow divider per section followed - # by every example in that section. + # in the manifest. Each section gets its own .home-section wrapper + # holding an eyebrow (tight, ~12px above its cards) and the + # section's grid; sections are spaced ~48px apart for clear + # separation. The shared outer .grid is gone — using one grid + # per section gives explicit control over the eyebrow's vertical + # relationship to its own cards vs the previous section. by_section: dict[str, list[dict]] = {} for example in list_examples(): by_section.setdefault(example["section"], []).append(example) - cards = [] + sections_html = [] for section, examples in by_section.items(): - cards.append(f'

{html.escape(section)}

') - for example in examples: - cards.append( - _replace( - '

__TITLE__

__SUMMARY__

', - { - "SLUG": html.escape(example["slug"]), - "TITLE": html.escape(example["title"]), - "SUMMARY": html.escape(example["summary"]), - }, - ) + card_markup = "".join( + _replace( + '

__TITLE__

__SUMMARY__

', + { + "SLUG": html.escape(example["slug"]), + "TITLE": html.escape(example["title"]), + "SUMMARY": html.escape(example["summary"]), + }, ) + for example in examples + ) + sections_html.append( + f'
' + f'

{html.escape(section)}

' + f'
{card_markup}
' + f'
' + ) content = _replace( _template("home.html"), - {"PYTHON_VERSION": html.escape(PYTHON_VERSION), "CARDS": "".join(cards)}, + {"PYTHON_VERSION": html.escape(PYTHON_VERSION), "CARDS": "".join(sections_html)}, ) return _layout( "Python By Example", diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 1758355..3799052 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.f3fd8c78f5ba.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} -HTML_CACHE_VERSION = '6e21b65d3855' +ASSET_PATHS = {'SITE_CSS': '/site.8f693d8043fe.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} +HTML_CACHE_VERSION = '3104d816ddfc' diff --git a/src/templates/home.html b/src/templates/home.html index 36f7380..6bbf92c 100644 --- a/src/templates/home.html +++ b/src/templates/home.html @@ -2,4 +2,4 @@

Python By Example

Learn Python with small, editable examples backed by the official Python __PYTHON_VERSION__ docs. Run each snippet in an isolated Dynamic Python Worker using the newest Python version currently supported by Cloudflare Workers/Pyodide.

-
__CARDS__
+__CARDS__ From 4144ef2d62ec7f6397cde5f4d904949f3b085fda Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 00:22:43 +0000 Subject: [PATCH 14/14] Tidy the dead space above the hero on the home page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- public/{site.8f693d8043fe.css => site.57a55415849b.css} | 3 ++- public/site.css | 3 ++- src/asset_manifest.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) rename public/{site.8f693d8043fe.css => site.57a55415849b.css} (98%) diff --git a/public/site.8f693d8043fe.css b/public/site.57a55415849b.css similarity index 98% rename from public/site.8f693d8043fe.css rename to public/site.57a55415849b.css index 4ccc4db..ee3d941 100644 --- a/public/site.8f693d8043fe.css +++ b/public/site.57a55415849b.css @@ -37,7 +37,8 @@ .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } - body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } + body:has(.hero) { padding-top: var(--space-2); } + body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; margin-bottom: var(--space-2); animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } } } diff --git a/public/site.css b/public/site.css index 4ccc4db..ee3d941 100644 --- a/public/site.css +++ b/public/site.css @@ -37,7 +37,8 @@ .hero { animation: hero-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 280px; } .hero h1 { animation: hero-h1-morph linear forwards; animation-timeline: scroll(root); animation-range: 0 240px; } .hero p { animation: hero-p-fade linear forwards; animation-timeline: scroll(root); animation-range: 0 140px; } - body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } + body:has(.hero) { padding-top: var(--space-2); } + body:has(.hero) header { opacity: 0; background: rgba(245, 241, 235, 0); box-shadow: none; margin-bottom: var(--space-2); animation: header-emerge linear forwards; animation-timeline: scroll(root); animation-range: 40px 240px; } body:has(.hero) header .brand { filter: blur(4px); transform: scale(0.88); animation: brand-focus linear forwards; animation-timeline: scroll(root); animation-range: 80px 240px; } } } diff --git a/src/asset_manifest.py b/src/asset_manifest.py index 3799052..915e4d8 100644 --- a/src/asset_manifest.py +++ b/src/asset_manifest.py @@ -1,3 +1,3 @@ # Generated by scripts/fingerprint_assets.py. Do not edit by hand. -ASSET_PATHS = {'SITE_CSS': '/site.8f693d8043fe.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} -HTML_CACHE_VERSION = '3104d816ddfc' +ASSET_PATHS = {'SITE_CSS': '/site.57a55415849b.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.a4a7766e1b9b.js'} +HTML_CACHE_VERSION = '324f7ab4825b'