Back to Pondering SPARK Edition S.P.A.R.K. Chat Log  •  07/06/2026
S.P.A.R.K. with AI — Development Dialog

Building the Pondering SPARK Edition

From a Stage 7 legacy app to a fully integrated TNT simulation —
three font algorithms, string manipulation, and the wisdom of Pinky.

For Students — How to Read This Page

This log documents the session that upgraded the Stage 7 Pondering app into the 2026 SPARK Edition. The app is built around a straightforward interaction — click, get a Pinky quote — but its real subject is text and strings: how long is a string, how wide is it when rendered, and three completely different approaches to answering “what font size should I use?”

Watch for Prompt Critique boxes (amber) and Design Decision boxes (teal). The most educationally rich sections are the three font-sizing algorithms and the createPhraseSnippets() analysis — both involve classic CS concepts dressed in cartoon clothing.

🎯
S — Set Goal The session began with an analysis of what the app already does well and where the visual and structural gaps are relative to the 2026 TNT standard.
klp

I’d like a brainPonder app upgrade. This app validates and works as desired. I like the fonts and the color scheme since it matches the Pinky and the Brain genre, although a gradient on the main page might add more visual interest. The original intent of the app was to show varying means of calculating font size. While we will keep (and link to) the original for comparison, let’s create: brainPonderSparkIndex.html, brainPonderSparkStyles.css and bpSparkSketch.js and ponderPhrasesSpark.js. I’d like a brainPonderChatlog.html too. I’m open to other upgrades to make the Spark version more professional and easy to understand. I like the playful banter in the ‘About’ area. Keep what you can but I’m open to it being reduced. Use the chatlog to educate users about the important string manipulation concepts.

Prompt Critique — A Comprehensive Brief That Specifies Constraints and Latitude

This opening prompt is unusually complete. It provides:

  • Current state assessment: “validates and works as desired” — same signal as the Magic 8 Ball prompt, telling the AI the logic is sound
  • A specific visual improvement suggestion: “gradient on the main page” — actionable rather than vague
  • Explicit file names: four specific files are named, which locks the architecture before the first line is written
  • Educational intent: the chatlog should “educate users about string manipulation” — the chatlog is specified as a teaching artifact, not just documentation
  • A tone constraint: “keep what you can of the playful banter” — a qualitative limit that prevents the AI from sanitising all the personality out of the app

The openness at the end (“I’m open to other upgrades”) is a productive invitation when paired with all the constraints above. Without the constraints, “open to upgrades” would be license to rewrite everything. With them, it means “within these boundaries, use judgment.”

GitHub Copilot

After reading all files, here is the app inventory and what the SPARK Edition changes:

Files kept unchanged:

  • brainPonderIndex.html — Stage 7, fully functional, linked for comparison
  • styles/siteStyles.css — original styles, preserved
  • scripts/pbSketch.js — original sketch, preserved
  • scripts/ponderPhrases.js — original data, preserved

New SPARK files:

  • ponderPhrasesSpark.js — same data, SPARK header and documentation
  • bpSparkSketch.js — enhanced sketch: gradient canvas, cleaner code, explicit DOM refs
  • styles/brainPonderSparkStyles.css — P&B deep purple/navy + ACME yellow palette
  • brainPonderSparkIndex.html — full TNT 2026 shell: navbar, hero, footer
  • brainPonderChatlog.html — this page

Key improvements in the SPARK sketch: Canvas gradient background (lerpColor() with 200 horizontal lines), explicit getElementById() for all DOM refs (no implicit globals), visually-hidden pattern instead of custom hideInfo/showInfo classes, and standardised function comments aligned with the teaching goals.

Design Decision — When to Create New Script Files vs. Keep Originals

The Pondering SPARK Edition breaks the “keep scripts untouched” rule used for the Magic 8 Ball and Slot Machine. This is intentional and correct:

  • The Magic 8 Ball sphere algorithm was polished code with nothing to improve educationally. Touching it would risk introducing bugs with no benefit.
  • The Pondering sketch (pbSketch.js) was a working development script with implicit globals, debug-era variable names, and unused code paths. The SPARK Edition benefits from a clean, well-commented version.

The rule is not “never touch the scripts” — it is “only touch the scripts when the change produces a measurable educational or quality improvement, and the original is preserved alongside for comparison.” Both conditions are met here: bpSparkSketch.js is more readable and more annotated than pbSketch.js, and pbSketch.js is still intact.

Visual Improvement — The Canvas Gradient
🔍
A — Analyze The user requested “a gradient on the main page.” There are two distinct places a gradient could live — the CSS page background and the p5.js canvas itself — and they require entirely different techniques.
GitHub Copilot

The gradient is applied in two places:

1. CSS (hero and page sections): Standard linear-gradient() on #hero over the ACME Labs image. This is the gradient the user sees before and around the app.

2. p5.js canvas (inside the drawing area): The canvas is opaque — CSS gradients cannot show through it. To gradient the canvas, the sketch draws 200 horizontal lines with lerpColor():

function drawBackground() {
    strokeWeight(2);
    for (var y = 0; y < myHeight; y += 2) {
        stroke(lerpColor(bgTop, bgBottom,
               map(y, 0, myHeight, 0, 1)));
        line(0, y, myWidth, y);
    }
}

lerpColor(a, b, t) interpolates between two p5.js color objects where t ranges from 0 (full a) to 1 (full b). map(y, 0, myHeight, 0, 1) converts the pixel position into that 0–1 range. The result: top = muted slate blue, bottom = deep purple. 200 draws per frame (stepping by 2) is negligible for a 400×400 canvas.

Design Decision — CSS Gradients and Canvas Are Separate Contexts

This is the same fundamental concept documented in the Slot Machine chatlog (FA icons vs canvas), restated for gradients:

  • A CSS linear-gradient is painted by the browser’s layout engine on the HTML background layer. It cannot affect anything inside a <canvas> element, which renders in its own independent drawing context.
  • A p5.js gradient is drawn programmatically, pixel row by pixel row, using the canvas 2D drawing API. It has no concept of CSS.

When a student asks “why can’t I just use CSS to style the canvas?” the answer is that CSS can style the element (border, border-radius, box-shadow) but not the drawing inside it. Everything inside the canvas is the exclusive domain of the JavaScript drawing API. The background colour, the text colour, the shapes — all of it must be specified in code.

Teaching Moment — Three Ways to Size a Font
🔍
A — Analyze The core pedagogical content of this app: three different algorithms for computing a font size from a string. Each reveals a different layer of how text works in code.
GitHub Copilot

All three methods solve the same problem: given a string of unknown length, what font size will make it readable inside a fixed canvas width? They differ in how they define “length.”

Method 1: Map
numChars = msg.length;
if (numChars < 50)       fontSize = map(numChars, 5, 30, 100, 60);
else if (numChars < 100) fontSize = map(numChars, 50, 100, 40, 15);
else                     fontSize = map(numChars, 100, 120, 30, 20, true);

msg.length counts all characters including spaces and \n. Three hand-tuned ranges partition the expected message lengths. Fast, but brittle — a message that is 49 characters gets a different formula than one that is 50.

Method 2: Root
numChars = msg.length;
fontSize = myWidth / Math.round(Math.sqrt(numChars));

Square root provides compression: sqrt(4)=2, sqrt(16)=4, sqrt(100)=10. So 100 characters (25× more than 4) produces only 5× smaller font. This keeps short and long messages both readable. One formula. No buckets.

Method 3: Loop
fontSize = 120;
textSize(fontSize);
var w = textWidth(msg);
while (w > myWidth * 2.4) { fontSize--; textSize(fontSize); w = textWidth(msg); }

textWidth() is a p5.js function that returns the rendered pixel width of a string at the current textSize. It is NOT a string property — it is a canvas measurement. The loop shrinks the font until the text fits. Slowest but most accurate.

Design Decision — String.length vs textWidth(): Two Different Questions

This is the central insight of the font-sizing lesson, and it is worth stating explicitly because students routinely confuse the two:

  • msg.length — a property of the JavaScript string. Answers: how many characters? “W” and “i” have the same .length contribution: 1. “WWW” and “iii” both have .length === 3.
  • textWidth("WWW") — a p5.js canvas function. Answers: how many pixels wide is this string when rendered at the current font and size? At the same font size, “WWW” is roughly three times wider than “iii”.

For most proportional fonts, msg.length is a reasonable proxy for visual width. That is why Map and Root produce acceptable results. But for precision layout — particularly when the string contains lots of wide characters or the font is unusual — only textWidth() gives the real answer.

The teaching experiment: switch to the Loop method, then use the “Jump to response” dropdown to select a response full of wide characters (like “Wuh... I think so, Brain”) and one full of narrow characters. Compare the font sizes the Map and Loop methods produce. The Loop method will produce tighter, more accurate fits.

Teaching Moment — String Manipulation in createPhraseSnippets()
GitHub Copilot

createPhraseSnippets() builds the short labels for the dropdown menu. It takes the end of each response (the punchline) rather than the beginning. Here is the key inner loop:

for (var ndx = p.length - 1; ndx >= 0; ndx--) {
    var ch = p[ndx];
    temp = ch + temp;   // prepend: ch goes BEFORE what we have so far
    count++;
    if (count > approxLen && ch === " ") {
        temp = "..." + temp;
        break;
    }
}

Three string techniques in twelve lines:

  1. Backward iteration: ndx = p.length - 1 starts at the last character and counts down. This is how you approach “take the last N characters” without slice(-N).
  2. String prepending: temp = ch + temp puts the new character before the accumulation. Because we are iterating in reverse, this restores the original left-to-right order in temp.
  3. Word-boundary detection: ch === " " checks for a space character. After approxLen characters, the loop waits for the next space before stopping. This avoids cutting off mid-word.
Design Decision — Why Iterate Backwards to Build a Suffix?

A modern JS developer might write this as p.slice(-approxLen).trimStart() and move on. The original developer chose the manual backward loop, and that choice is educationally significant:

  • It demonstrates that String.prototype.slice() is not magic — the underlying operation is simply an index-based traversal.
  • The prepend pattern (temp = ch + temp) illustrates that string concatenation is directional. Adding to the left vs adding to the right produces different results and can be used to reverse or reorder substrings.
  • The word-boundary check shows that “take the last 25 characters” and “take the last 25 characters without splitting a word” are different problems requiring different solutions.

For production code, use slice(). For teaching string fundamentals, the manual loop exposes the mechanism. The SPARK sketch keeps the manual loop for this reason and adds a comment explaining what temp = ch + temp is doing, which the original lacked.

Teaching Moment — No-Repeat Random Selection
GitHub Copilot

The app guarantees that clicking the canvas shows each response exactly once before repeating. The implementation is a “deal without replacement” algorithm:

do {
    anNdx      = int(random() * n);
    alreadyUsed = usedNdxes.includes(anNdx);
    if (!alreadyUsed) {
        usedNdxes.push(anNdx);
    }
} while (alreadyUsed);

A do-while loop runs at least once, then keeps rolling new random indices until it finds one not in usedNdxes. Array.includes() does a linear scan — O(n) per check. With 70 responses this is negligible. When responseCount === n, the array resets and the cycle starts over.

The stats panel below the canvas shows every index used so far in the current cycle. Students can watch the no-repeat guarantee in action and see the reset when all responses are exhausted.

Design Decision — Displaying Algorithm State as Pedagogy

The stats panel under the canvas in the SPARK Edition shows three live values:

  • charsmethodfontSize — the font calc decision: how many characters, which algorithm, and the resulting size
  • Response N of 73 — progress through the cycle
  • Used: 0, 14, 7, 22, ... — the growing list of used indices

This is the same principle as “The Math Behind the Ball” in the Magic 8 Ball SPARK Edition: developer debug output transformed into intentional educational transparency. The stats panel makes the algorithm visible to anyone who is paying attention.

Notice that font size changes as you switch methods with the same message visible. The canvas redraws (with a new fade animation) every time the method radio button changes, which gives students an immediate visual comparison without needing to remember what the previous method produced.

Refinement — The About Section and Layout Decisions
✏️
R — Refine Two refinement decisions shaped the user-facing presentation: how much of the original About content to keep, and whether the two-column layout (image left, canvas right) should change.
GitHub Copilot

About section: The original has significant content — the full P&B flavor quote, Stage 7 history, detailed code snippet in a <pre> block, and two figure images. In the SPARK Edition:

  • Kept: the P&B opening exchange, the “narf!” closer, the three-method explanation, links to chatlog and original
  • Moved: the detailed code snippet moved to the chatlog, where it has educational framing around it
  • Removed: Stage 7 development history (original-only content), one of the two figure images (the cartoon world image didn’t add information)

Layout: The two-column layout is preserved because it is correct for this interaction model. The controls (poster, radio buttons, dropdown) are always visible alongside the canvas — students can switch methods without losing sight of the image that sets the P&B context.

Design Decision — Where Code Samples Belong

The original app displayed the three font-sizing functions in a <pre> block inside the About panel. This is a reasonable design for a developer-facing app, but it has a problem: the code is shown without context, without explanation, and without connecting it to what the student is watching on screen.

Moving the code to the chatlog solves all three problems:

  • Each function gets its own framed section with a plain-English explanation of what it does and why it works
  • The comparison between methods is documented in prose, not just laid out in raw code
  • Students who just want to use the app see a clean About section; students who want to understand the code have a dedicated page

The pattern: the app surface shows what happens; the chatlog explains why. Raw code in an About panel tries to do both at once and does neither well.

💡
K — Know Five principles from this session that apply to any app dealing with text, strings, and canvas rendering.
Session Takeaways
  1. String.length counts characters; textWidth() measures pixels. They are different questions. .length is a JavaScript string property available anywhere. textWidth() is a p5.js canvas function that requires an active font and font size. Use .length for fast approximations; use textWidth() when you need layout precision.
  2. Square root is a natural compression function. Math.sqrt(n) grows much slower than n. When you divide by it, you get a font size that shrinks reasonably as message length grows — not catastrophically for long messages, not gigantic for short ones. No if-branches, no hard-coded thresholds. One formula that handles the full range.
  3. Iterating backwards through a string is the manual version of slice(-n). The temp = char + temp prepend pattern is inefficient but transparent. Production code uses slice(); teaching code uses the loop so students can see the mechanism before they use the shortcut.
  4. No-repeat random selection is “deal without replacement.” Track used indices in an array. Check with .includes() before accepting a new draw. Reset the array when exhausted. This guarantees full coverage before any repeats — the same principle used in card games, quiz shuffles, and playlist shuffles.
  5. CSS gradients and canvas gradients are unrelated. CSS can style the canvas element (border, border-radius). Everything inside the canvas is drawn by JavaScript. Use lerpColor() + map() to draw gradient rows in p5.js. The canvas rendering context knows nothing about CSS.
What This Refactor Produced
FileStatusWhat it does
scripts/pbSketch.jsUnchangedOriginal Stage 7 sketch — preserved for comparison
scripts/ponderPhrases.jsUnchangedOriginal data file — preserved for comparison
brainPonderIndex.htmlUnchangedOriginal Stage 7 — preserved for comparison
styles/siteStyles.cssUnchangedOriginal styles — preserved for comparison
scripts/ponderPhrasesSpark.jsNewSame dataset, SPARK documentation header
scripts/bpSparkSketch.jsNewEnhanced sketch: gradient canvas, explicit DOM refs, annotated font methods
styles/brainPonderSparkStyles.cssNewP&B deep purple/navy palette; ACME yellow accents
brainPonderSparkIndex.htmlNewFull TNT navbar/footer, hero, two-column layout
brainPonderChatlog.htmlNewThis page

All original Stage 7 files preserved untouched alongside the SPARK Edition.

Post-Session — Canvas Color and Page Depth
klp

I’d like to make some adjustments to the coloring. The gradient on the canvas makes the lettering harder to read. Let’s make it a constant value of #44356A. To add some more ‘depth’ to the app, let’s use a background gradient that goes from a darker color, #0E0828 to a lighter color, #2A2A5D as sampled from the artwork. Of course, add this modification to our chatlog.

Prompt Critique — Two Problems, Two Solutions, Correctly Separated

This prompt identifies two distinct issues and proposes a different solution for each. That separation is important:

  • Problem 1 (canvas): the gradient background competes with the text. The solution is a solid colour. This is a rendering decision — it lives in the p5.js sketch.
  • Problem 2 (page): the page background lacks visual depth. The solution is a CSS gradient. This is a presentation decision — it lives in the stylesheet.

The detail “as sampled from the artwork” is a professional design instinct. Rather than picking colours arbitrarily, the gradient values were taken directly from the ACME Labs background image — ensuring the page palette is coherent with the hero image it accompanies.

The instruction “add this to our chatlog” continues the pattern established earlier in this session: post-session field-testing is part of development, and the chatlog documents the full process, not just the initial build.

GitHub Copilot

Two changes, two files:

bpSparkSketch.js — canvas background: Replaced the 200-line lerpColor() loop in drawBackground() with a single call:

function drawBackground() {
    background(bgSolid);   // bgSolid = color('#44356A')
}

brainPonderSparkStyles.css — page background: Changed body from a solid #0e0828 to the sampled gradient, and set #gameSection to transparent so the gradient shows through:

body {
    background: linear-gradient(180deg, #0e0828 0%, #2a2a5d 100%);
    min-height: 100vh;
}
#gameSection {
    background: transparent;
}

The hero section keeps its own image background. The footer keeps its own dark background. Only the game content area shows the gradient, framing the cards against the lighter purple mid-tones.

Design Decision — Gradients Belong on the Page, Not on the Canvas Text Background

The canvas gradient was visually interesting but functionally harmful: the fade from blue-gray to deep purple across the canvas height means the text colour (pinkyNose, a warm orange-pink) had a different contrast relationship at the top vs the bottom of each message. Short messages that sit near the bottom were particularly affected.

A solid canvas background solves the problem definitively: one background colour, one contrast relationship, consistent for every message at every font size. The chosen value #44356A (a medium purple that sits between the page’s dark and light endpoints) reads as part of the palette without competing with the text.

The page gradient provides the depth that the canvas gradient was trying to create — but in the right place. CSS gradients are processed once by the browser layout engine; they have no rendering cost. The canvas gradient required 200 draw calls per frame during animation. Moving the gradient to CSS gives more visual impact with less computational work.

The principle: use CSS for everything the page can control. Reserve canvas drawing calls for things only the canvas can do. A background that does not change with each response is not a canvas concern.

Post-Session — Restoring the Classic Poster
klp

In the original design, I featured a poster of Pinky and the Brain in the ‘about’ area. I actually liked it; what do you think about putting it in our new version too?

Prompt Critique — Asking for Opinion Before Implementing

The user says “I actually liked it” — staking a position — and then asks “what do you think?” rather than just saying “add it.” This opens a design conversation before any code is written.

That question is worth asking here because the original poster was in a white-background Bootstrap collapse panel. The SPARK About section uses a dark-background styled panel. The poster itself is fine in both contexts, but the caption styling, figure sizing, and float mechanics all need to be reconsidered for the new environment. Asking first means the integration is designed, not just copied.

GitHub Copilot

Yes — the poster adds genuine visual identity to the About section. It establishes the P&B context immediately for anyone who opens the panel, and it creates continuity between the original and SPARK versions for students comparing the two.

In the original, the figure lived in a white panel with Bootstrap default styles. The SPARK About body is a dark panel with amber left-border and muted text. Three things needed adapting:

  • Caption colour: changed from the original default dark text to rgba(255,255,255,0.40) — legible but not competing with the body text.
  • Border-radius: added 6px to the image to match the SPARK card aesthetic.
  • Float geometry: see below.
Design Decision — Float Geometry: How the Poster Sits in the Text

The poster uses CSS float: right. Understanding what that means geometrically is worth a close look:

  • Float removes an element from normal flow. The poster is pulled out of the text column and anchored to the right edge of its containing block (the .about-body div). The remaining text wraps around it on the left.
  • Width: 38% of the container, capped at 155 px. width: 38% means the poster is always proportional to the panel it sits in — on a wide screen the cap kicks in so it never gets distractingly large. On a narrow mobile screen it shrinks with the panel. These two rules together define the valid display range: below 155 px ÷ 0.38 ≈ 408 px container width, percentage governs; above that, the cap governs.
  • Margin: ms-3 mb-2 (Bootstrap) = 1rem left, 0.5rem bottom. These push the text away from the poster and give breathing room below it before the paragraph continues.
  • Clearfix: <div class="clearfix"></div>. A float only interacts with elements that come after it in source order. Without a clearfix, the link row at the bottom of the About body would start immediately to the left of a still-floating image if the text is short. The clearfix div — styled with clear: both — forces everything after it below the tallest float, regardless of which is taller: poster or text.

The clearfix rule is one of the oldest CSS layout techniques (predating Flexbox by a decade) and still the right tool here: Flexbox and Grid don’t interact with floats. When you need text to wrap around an image, float + clearfix remains the correct pattern.

Post-Session — Mojibake in the Data File
klp

I notice in the ponderPhrasesSpark.js page, there are ‘weird’ symbols like HTML entities that are not rendering properly. For example there is a phrase: wouldn’t rather than wouldn’t. Another is a bad rendering of a larger dash: Soupâ€" rather than Soup —. Can you find these problems and fix them?

Prompt Critique — Naming the Symptom and Providing a Concrete Example

This is a well-formed bug report. It provides:

  • The exact symptom: specific bad characters shown as they appear in the file
  • The expected output: what the characters should be
  • Two examples: one for the apostrophe problem, one for the em-dash — which turns out to be important, because they are different sequences requiring separate fixes

The user noticed the problem by looking at the raw file content, not by testing in a browser. That is a good habit: inspecting data files visually as well as testing them in their execution context.

GitHub Copilot

The problem is mojibake — a Japanese word for “unintelligible characters.” Three types were found in the file:

  • ’ — right single quotation mark (apostrophe)
  • — — em dash (3 occurrences)
  • Ã©é — e with acute (passé)

The root cause: the original ponderPhrases.js was created or handled by a tool that read UTF-8 bytes as Windows-1252, permanently converting each multi-byte UTF-8 character into 2–3 visible but wrong characters. PowerShell faithfully copied those corrupt characters into ponderPhrasesSpark.js.

The fix required three separate discoveries before the final solution worked: identifying exact Unicode codepoints via inspection, realising that apostrophe and em-dash use different third bytes, and working around PowerShell’s .Replace() overload resolution which silently picked the wrong Replace(char, char) variant. The full technical breakdown is in DWR Entry 14.

Design Decision — This Became DWR Entry 14

The encoding fix was straightforward in principle but unexpectedly complex in execution. The three obstacles — visual ambiguity of mojibake characters, different third bytes for different character types, and PowerShell overload resolution — each required a separate diagnostic step. That combination of non-obvious problems is exactly the DWR/Eureka profile: a pitfall worth documenting so the next person can solve it in minutes rather than an hour.

The file ponderPhrasesSpark0.js was preserved alongside the corrected version as a before/after reference, allowing side-by-side comparison of all 9 mojibake sequences. This is the same “preserve the original for comparison” principle applied to data files, not just HTML.

Post-Session — The S.P.A.R.K. Card Tile Cap
klp

I notice in our tiles in the explore page: the list of apps in some cases is getting longer. In the Spark area, there are now 6 apps. What do you think about capping how many apps show in the tile and show all of them with the side callout? I’m thinking: we set a variable, tileAppListMax, and use it to decide how many apps to show on the tile itself. Before we do anything, talk to me about the pros and cons of this approach.

Prompt Critique — Proposing a Mechanism, Then Asking for Analysis

The prompt names a specific technical solution (tileAppListMax) and then immediately asks for pros and cons before committing to it. That sequence is exactly right. Proposing a direction signals that thinking has already happened; asking for analysis before acting invites a genuine evaluation rather than immediate execution.

The timing is also right: the Pondering SPARK addition was the event that pushed the S.P.A.R.K. card from 5 to 6 items, making the problem visible. Noticing a design issue the moment it crosses a threshold — rather than later — is good field-testing discipline.

GitHub Copilot

The tile and the offcanvas serve two different students with two different goals. The tile is a signpost for discovery: scan, recognise the category, pick one of 2–3 representative apps. The offcanvas is a catalogue for browsing: open it, see everything. When a tile list grows to six items, it is doing the offcanvas’s job badly — too long to scan, too short to be complete.

The tileAppListMax variable enforces quantity but not quality — auto-hiding beyond index 3 would hide the wrong items if they weren’t added in priority order. It also renders HTML that is immediately hidden, which is wasteful and confusing to read. The better solution is editorial discipline: cap at 3 curated items, chosen intentionally. Everything ships to the offcanvas regardless.

For the S.P.A.R.K. card: keep Yahtzee SPARK, Magic 8 Ball SPARK, and Pondering SPARK — three interactive apps that illustrate different facets of the method. Remove Git Workflow, Font Awesome, and Purple People Eater from the card; they remain fully accessible in the offcanvas.

Design Decision — This Discussion Produced a Site-Wide Principle

The tile-cap conversation started here but the rule it produced applies to every category on the Explore page, not just S.P.A.R.K. It was documented as Entry 13 in the DWR & Eureka Moments log, where it lives as a standing architectural rule for the whole site.

The short form of the rule: card tile = 3 curated featured apps, editorial choice, HTML only; offcanvas = complete inventory, everything ships here. The “See all” button is the bridge between them — its purpose is to exist and be clicked, not to be bypassed by making the tile do the offcanvas’s job.