TechNoviceTools — Est. 2015

Danger Will Robinson!
& Eureka Moments

A running log of coding disasters survived —
and surprising discoveries worth remembering.

What Are DWR & Eureka Moments?

Robot warning Will Robinson — Lost in Space

D.W.R. — Danger Will Robinson!

Remember the Robot from Lost in Space flailing its arms and warning young Will Robinson of impending doom? That’s us — warning you. A DWR entry is a coding pitfall we stumbled into so you don’t have to — or at least so you recognize the cliff when you’re standing at the edge.

Never seen the Robot in action? Fix that immediately. →

Archimedes in his bathtub — the original Eureka moment

Eureka Moments

Named for Archimedes’ legendary bathtub shout. A Eureka entry is a discovery — something cool we figured out, a technique that clicked, or a tool that made us say “Wait, you can DO that?!” Worth writing down so we can find it again.

Never seen the legend in action? Fix that immediately. →

Both are part of the same learning loop: blow something up, figure out why, write it down. That’s the TNT way.

Moments & Milestones

Newest entries at the top. Click any title arrow to expand the full story.

Date App / Topic Type
08/05/2026

Password Generatorchrome://inspect on iPhone: Desktop-Class Console Logging in Your Pocket

Read more…

After reading Ask Copilot Entry #039 about breadcrumb console.log() traces, TTG wanted to follow them on an iPhone using Chrome — no laptop, no cable, no DevTools panel. The discovery: Chrome on iOS has a built-in log collector hiding in plain sight at a special address. One tap starts it. Open any TNT app in another tab, use it, then switch back and read the breadcrumbs. It works exactly as described.

The Discovery: One Address, No Setup

In the Chrome app on iPhone, open a new tab and type the following into the address bar:

chrome://inspect

Chrome opens a page titled Javascript Console with a single Start Logging button. Tap it — the button changes to Stop Logging, confirming capture has begun. Now switch to any other tab, open an app, interact with it, and switch back to chrome://inspect. Every console.log() message from every tab appears in the log, grouped by page URL. No extensions, no accounts, no developer mode required.

Proof: The Password Generator Breadcrumb Trail on a Real iPhone

TTG opened the Password Generator in a separate tab, changed a setting, moved the length slider, and pressed Copy. Switching back to chrome://inspect revealed the complete execution trail — the same breadcrumbs documented in Ask Copilot Entry #039, now visible on an iPhone screen:

iPhone Chrome chrome://inspect Javascript Console showing Password Generator breadcrumb console.log output: init, updatePassword, applySpecialFeatures, copyToClipboard
Chrome on iPhone — chrome://inspect → Javascript Console, showing the Password Generator’s six-breadcrumb execution trail on a real device.

Six breadcrumbs: ...init..., ...updatePassword..., ...applySpecialFeatures..., ...updatePassword..., ...applySpecialFeatures..., ...copyToClipboard.... The log shows exactly which functions ran, in what order, triggered by which interaction — on a phone, with no laptop in the room.

Step-by-Step

  1. Open Chrome on iPhone (this is a Chrome-specific feature — Safari uses a different route)
  2. Open a new tab and type chrome://inspect in the address bar
  3. Tap Start Logging
  4. Open any app or page in another tab and interact with it
  5. Return to the chrome://inspect tab and read the captured output
  6. Tap Stop Logging when done — the log is held in memory only and clears when you leave the page

How This Compares to Desktop DevTools

FeatureDesktop DevTools ConsoleiPhone chrome://inspect
Access F12 → Console tab New tab → chrome://inspect
Output timing Live — appears as it happens Captured — read after switching back to the inspect tab
Scope One page at a time All open tabs, grouped by URL
Interactive console Yes — type and run JS live No — read-only log viewer
Network / Elements panels Yes — full suite No — console output only
Cable or Mac required No No

Why This Matters

Many TNT apps behave differently on mobile than on desktop — touch events, viewport constraints, audio playback quirks (see Ask Copilot Entry #026 on p5.sound vs HTML5 Audio on iOS), and browser-specific rendering differences. Previously, diagnosing those on an iPhone meant either a Mac with Safari’s Web Inspector connected via USB, or a third-party remote debugging service. chrome://inspect is neither — it is built into Chrome for iOS, requires no setup, and is available to any student who has the app. It is not a full DevTools replacement, but for reading console.log() breadcrumbs on a real device, it is exactly sufficient.

The rule: when something behaves differently on mobile and you cannot reproduce it on desktop, plant breadcrumbs in your functions, open chrome://inspect on your iPhone, start logging, reproduce the behavior, and read the trail. The breadcrumbs are the same ones you drop for desktop debugging — they just surface in a different window. Same technique. Same output. Different pocket.

Related: Ask Copilot Entry #039 (breadcrumbs as a debugging technique and the web-navigation pattern that shares the name) • Password Generator (the app shown in the screenshot, with deliberate console.log() breadcrumbs left in every key function for exactly this purpose).

Eureka
08/05/2026

DWR & Eureka — The Entry Count Said 88. Adding > Said 81. A Human Spotted the Real Problem: It Still Wasn’t 29.

Read more…

The filter bar on this very page reported “Showing all 88 entries”. There were 28 entries at the time. No entries disappeared, the table rendered perfectly — but the count was wildly wrong and the filter was silently operating on rows it was never supposed to touch. This is the story of three selector attempts and the human who caught that the second one was still wrong.

Step 1: The Original Bug — 88 entries

The JavaScript that counts and filters entries used this selector:

document.querySelectorAll('#moments tbody tr')

That space between tbody and tr is a descendant combinator. It means: “find every <tr> element anywhere inside #moments tbody, at any depth, no matter how deeply nested.” Many DWR entries contain comparison <table> elements inside their <details> panels — the Case Sensitivity table, the Bootstrap SRI table, the textContent vs innerHTML table, and so on. Every row in every one of those inner tables is also a <tr> that lives inside #moments tbody. The selector found all 60 of them and added them to the count alongside the 28 real entries: 88 total.

Step 2: The First Fix Attempt — Still 81

The obvious fix was to add a child combinator (>) so only direct children of tbody were selected:

document.querySelectorAll('#moments tbody > tr')

This looked right — tbody > tr means “only <tr> rows that are direct children of a <tbody>.” But the count dropped to 81, not 28. TTG caught this immediately and asked the right question: “I don’t think we have 81 entries!”

Why was it still wrong? Because the selector reads in two parts: first it finds any <tbody> that is a descendant of #moments — which includes the <tbody> elements of every nested inner table — and then finds the direct <tr> children of each of those. So the inner table rows were still being matched, just fewer of them (the ones that were direct children of their respective inner <tbody> elements rather than deeper descendants).

Step 3: The Correct Fix — 29

The solution was to chain the child combinator all the way from the table itself:

document.querySelectorAll('#moments > tbody > tr')

Now the selector reads as a strict chain: find the <tbody> that is a direct child of #moments (there is only one — the outer one), then find only <tr> rows that are direct children of that specific <tbody>. The nested inner tables have their own separate <tbody> elements, but those are not direct children of #moments — they are nested several levels down — so the selector never reaches them. Count: 29. Correct.

The Three-Step Summary

SelectorCountWhy
#moments tbody tr 88 Every <tr> at any depth inside any <tbody> in the table
#moments tbody > tr 81 Direct <tr> children of any <tbody> in the table — still finds inner tables’ tbodys
#moments > tbody > tr 29 ✓ Direct <tr> children of the one outer <tbody> that is a direct child of the table

The CSS Combinator Zoo: Four Ways to Relate Two Elements

CSS gives you four different ways to express the relationship between a parent selector and a child selector. They look almost identical but behave very differently:

CombinatorSyntaxMeaningExample match
Descendant A B (a space) Any B inside A, at any depth tbody tr — every <tr> anywhere in the table body
Child A > B Only B elements that are direct children of A table > tbody > tr — only the outer entry rows
Adjacent sibling A + B The B that immediately follows A h2 + p — only the first paragraph right after an h2
General sibling A ~ B All B elements that follow A (same parent) h2 ~ p — all paragraphs after an h2 at the same level

Visualizing the Structure

Think of your HTML as a family tree. The outer <tbody> is a direct child of the table. Each of the 29 entry <tr> rows is a direct child of that outer <tbody>. But each entry row contains a <details> panel, which contains inner <table> elements, which have their own <tbody> elements and <tr> rows. Those inner rows are grandchildren many levels deep — descendants of the table, but not direct children of the outer <tbody>.

#moments (table)
  ├─ tbody              ← direct child of #moments
  │   ├─ tr          ← direct child of outer tbody = Entry row ✓
  │   │   ├─ td
  │   │       ├─ details
  │   │           ├─ table
  │   │               ├─ tbody   ← NOT a direct child of #moments
  │   │                   ├─ tr   ← inner table row — should NOT be counted
  │   │                   └─ tr   ← inner table row — should NOT be counted
  │   └─ tr          ← direct child of outer tbody = Entry row ✓
  …

Why the First Fix Was Not Enough — and Why a Human Caught It

This is a genuine two-DWR: the original bug and the first attempted fix. The first fix was logically plausible — adding > to restrict to direct children sounds like exactly the right idea — but it was applied to the wrong relationship in the chain. Fixing tbody tr to tbody > tr restricted the tr step to direct children but left the tbody step as a free-range descendant search, which meant all the inner tbodys were still in scope.

TTG spotted the residual error not by reading the selector, but by checking the output against known reality: “I don’t think we have 81 entries.” That simple sanity check — does the number make sense? — is something an AI will not always volunteer without prompting. The AI had fixed the reported symptom (88 was wrong), but produced a new wrong number (81) without flagging it as suspicious. A human glancing at the count and knowing there are roughly 29 entries caught in one second what the AI missed entirely.

The rule: when writing a querySelectorAll() for a tag that also appears in nested structures, apply the child combinator (>) at every step in the chain where you mean “direct child only” — not just the last step. And always verify counts against known reality. If the number doesn’t feel right, it probably isn’t.

DWR
08/01/2026

AI-Assisted Development — Markdown Files as Persistent Session Memory: When an Important Process Emerges, Write It Down Immediately

Read more…

TTG observed: “I have a dialog with you with lots of insights or action steps and then our session ends. I might not have documented everything. When an important process needs future reference, I’ve been able to read the document myself and/or give it to you so you know the past context. It’s been wonderful!”

This is one of the most practical patterns to emerge from a year of AI-assisted development at TNT. It deserves naming, documenting, and treating as a deliberate habit.

The Discovery

AI sessions end. The conversation history disappears. The AI starts every new session with no memory of the previous one. But files persist. A markdown file written during a session outlives the session by an arbitrary amount of time — days, months, years. That asymmetry is the insight. Any piece of knowledge worth keeping should be written into the workspace as a file, not left to live only in the chat history.

The secondary discovery: these files serve triple duty simultaneously.

  1. Human reference document. You can read it yourself later without reconstructing the conversation. Step-by-step processes that took 30 minutes to work out are instantly available in a form you can follow without the AI present at all.
  2. AI context restoration. Attach or paste the file at the start of a new session and the AI has full context immediately. The session can resume where it left off rather than rebuilding context from scratch through questions.
  3. Institutional documentation. The file is a record that someone else (a student, a colleague, a future version of the teacher) can read to understand why something was built the way it was, and how to maintain it.

TNT Examples Already in the Workspace

FileWhat it capturesWhen it became essential
accessSHuntKeys.md 5-step process for deploying .htpasswd and accessing answer keys Immediately — a multi-step server process with no visible interface
scavengerHuntStartPlan.md Adventures section architecture, workflow, and build protocol Before the first file was created — design decisions needed anchoring
custom404Plan.md .htaccess ErrorDocument setup and absolute-path requirement When the deployment was non-obvious and the lesson needed preserving
suggestedMigrationProcess.md TNT legacy site migration approach When the scope was large enough that decisions needed to be tracked

When to Create a Markdown File

Not every conversation warrants a file. The signal that one is needed:

  • Multi-step deployment process with server-side steps, file naming conventions, or authentication setup — anything that involves the live server and is invisible in the browser.
  • Architectural decision that future work depends on — folder structure, naming conventions, protection strategy.
  • Process that will repeat — adding a new hunt, creating a new activity type, setting up a new section.
  • Non-obvious “why” — any decision where the reasoning would be lost if only the outcome survived.

If you find yourself explaining the same thing twice in two different sessions, that is the clearest signal: write it down once and link to the file instead.

The Rule

When an important process emerges in a session, write it into a .md file before the session ends. The file costs nothing. The alternative — reconstructing the process from memory in a future session — costs time, introduces errors, and may not be possible if the original conversation is gone. A markdown file is the cheapest insurance in the TNT workflow.

The irony is that this Eureka moment is itself documented in a DWR entry, which is itself a form of persistent documentation. And the document about accessing answer keys (accessSHuntKeys.md) is a direct product of the insight. The pattern is self-demonstrating.


If you need more insight into the nature of 'markdown' files, check out our Markdown vs Text Files document in resources.

Eureka
08/01/2026

Movie Clipsonerror="this.style.display='none'": Graceful Image Degradation — and the Invisible Deployment Gap It Creates

Read more…

The Conway’s Game of Life card appeared on the live server with no icon — but no broken-image symbol either. The card was clean. The SVG had simply never been uploaded from the local images/ folder to the live server. The browser silently hid the failure. The cause: every <img> tag in TNT carries onerror="this.style.display='none'".

How It Works

onerror is an HTML event attribute. On an <img> element it fires when the browser cannot load the image — 404, wrong path, network failure, or unsupported format. The attribute’s value runs as JavaScript. In TNT’s case:

onerror="this.style.display='none'"

Inside an inline event attribute, this refers to the element that fired the event — the <img> itself. The instruction is: set my own display to none. The image vanishes from the layout. The browser’s default broken-image indicator never appears.

Why This Is Good Practice

This is graceful degradation: failing silently without producing visual damage. For decorative elements — icons, card thumbnails — that are not structural, hiding a failed image is almost always better than showing a torn-photograph placeholder. The page continues to function; the surrounding content is unaffected. A movie-clip card without its icon is still a usable card.

Why This Is Also a Trap

The same mechanism that makes the page look clean makes the failure completely invisible. The page does not look broken. There is no indication that anything went wrong in the viewport. The 404 appears only in the browser console — a red row in the Network tab — which is only visible if DevTools is open and someone is looking. A casual browsing session produces no visible clue.

The Rule: Verify With DevTools, Not Just Your Eyes

After uploading files to the live server, do not trust the visual appearance alone. onerror will make the page look fine even when assets are missing. The correct verification is:

  1. Open DevTools (F12)
  2. Go to the Network tab
  3. Hard-reload (Ctrl+Shift+R)
  4. Look for red rows — any 4xx status is a missing asset

Thirty seconds. Catches every class of missing-asset error regardless of whether onerror is concealing it visually.

Keep onerror on all decorative images. The graceful degradation is correct. The deployment verification habit is what needs reinforcing: a clean-looking page is not the same as a correct page.

Caveat: Linked Images and the Fallback-src Pattern

If the <img> is wrapped in an <a> tag, setting display:none on the image makes the anchor invisible and unreachable by mouse — the link silently disappears along with the image. TNT’s Movie Clips cards use exactly this structure: each card icon is the only clickable element linking to its clip page. A hidden icon means a hidden navigation path.

The correct solution for linked images is a fallback src instead of hiding:

onerror="this.src='images/fallbackMovieIcon.png'; this.onerror=null;"

When the primary icon fails, onerror replaces the broken src with a neutral placeholder. The <a> stays visible because it again has a visible child — the link remains clickable. The this.onerror=null is essential: it disarms the handler after the first attempt, preventing an infinite loop that would occur if the fallback image were also missing.

TNT’s images/fallbackMovieIcon.png is a gray dashed-frame placeholder designed for this purpose — visually distinct from any real movie icon and immediately recognisable as “something should be here.” New movie clip card additions should use:

<img src="images/yourIcon.png"
     alt="Description"
     class="movie-icon"
     onerror="this.src='images/fallbackMovieIcon.png'; this.onerror=null;">

Existing cards on movie_clips.html retain display:none — their icons are present and verified, and this is a forward-looking standard only. Rule of thumb: display:none for standalone decorative images; fallback src when the image is the navigation element. Scenario 4 in demoMissingImages.html demonstrates this live.

Full write-up: Ask Copilot Entry #037. Scene of the crime: the Conway’s Game of Life clip page — its card icon on movie_clips.html was the one left off the live server. Live demo showing all three scenarios with a DevTools walkthrough: demoMissingImages.html. Related: DWR Entry #21 (case-sensitive filenames on Linux — another class of “works locally, breaks on live” upload error).

Eureka
08/01/2026

404.html — Server-Level Soft-Landing: ErrorDocument 404 in .htaccess, and Why Every Asset on a 404 Page Needs an Absolute Path

Read more…

TNT had a client-side soft-landing via linkGuard.js for broken internal links, but a raw browser 404 for any URL typed directly in the address bar. The fix is server-level and requires just two files. This entry documents the solution and the non-obvious technical requirement that makes it work correctly across all URLs.

The Solution: .htaccess ErrorDocument

A single line in a .htaccess file at the site root tells Apache to serve a custom page instead of its raw 404 response:

ErrorDocument 404 /404.html

The path must be absolute from the site root (leading /). Apache reads the .htaccess on every request — no server restart needed. The server still returns an HTTP 404 status code in the headers; the soft-landing changes only the visual content, not the status. This matters for SEO: a 200 response would allow search engines to index the error page as real content.

The Eureka: 404 Pages Must Use Absolute Paths for Every Local Asset

A custom 404 page is served for any missing URL on the site — including deeply nested ones. If the 404 page uses a relative path for a stylesheet or image, the browser resolves it relative to the missing URL’s directory, not the page’s actual location. The asset request also 404s. The page renders unstyled or broken.

Example: visitor requests technovicetools.com/SparkApps/missing.html. The server serves /404.html. The 404 page contains:

<link href="styles/tnt-base-styles.css">  <!-- relative path -->

The browser resolves this as /SparkApps/styles/tnt-base-styles.css — which does not exist. The stylesheet is not loaded. The 404 page arrives completely unstyled.

The fix is to make every local asset reference absolute from the site root:

Relative (breaks on deep URLs)Absolute (always works)
styles/tnt-base-styles.css/styles/tnt-base-styles.css
images/favicon-32x32.png/images/favicon-32x32.png
scripts/linkGuard.js/scripts/linkGuard.js
index.html (href)/index.html

CDN resources (Bootstrap, Font Awesome, Google Fonts) are already absolute URLs and require no change.

The Two-Layer Soft-Landing System

TNT now has two complementary guards:

  • linkGuard.js — client-side; intercepts broken internal link clicks before navigation; redirects to siteUnderConstruction.html. Requires a page to already be loaded.
  • ErrorDocument 404 — server-side; intercepts any missing URL including direct address-bar typos and stale external links; serves 404.html. Runs before any JavaScript loads.

Verification

After uploading both files to the live server, test by visiting a URL you know does not exist. Then open DevTools → Network tab → click the document row → check Status Code. It should read 404, not 200. If it reads 200, the server configuration is overriding the status code — check the host’s cPanel “Error Pages” panel for a conflicting setting.

The 404 page also carries <meta name="robots" content="noindex, nofollow"> as a second layer of SEO protection, reinforcing the 404 status code instruction to search crawlers.

Full explanation including both layers, deployment routes, and the Windows .htaccess naming restriction: Ask Copilot Entry #036. Planning notes: custom404Plan.md.

Eureka
08/01/2026

Ask Copilot Entry #033 — Orphaned <li>: Splitting a List with a Block Element Produces a Validator Error the Browser Silently Ignores

Read more…

Ask Copilot Entry #033 documents four methods for stripping CSS without an extension. Method 2 is a bookmarklet — which requires pasting a URL into a bookmark. To make that URL easy to copy, a Copy button and a <pre> block were inserted between list items 3 and 4 of an <ol>. The <ol> was closed before the <div> block, and item 4 was written as a stray <li> after the closing </div>. The page looked and functioned perfectly. Then the validator reported:

Error: Element “li” not allowed as child of element “div” in this context.

Why It Happened

The structure that caused the error looked like this:

<ol>
    <li>1. Right-click the bookmarks bar…</li>
    <li>2. For the name, type Strip CSS.</li>
    <li>3. For the URL, paste exactly this:</li>
</ol>
<div>
    <!-- Copy button + <pre> with the bookmarklet URL -->
</div>
<li style="list-style:none;">4. Save the bookmark.</li>  <!-- INVALID -->

The <li> on the last line is a direct child of a <div> — which is invalid HTML. The HTML specification only allows <li> as a child of <ol>, <ul>, or <menu>. A <div> is none of those. The browser is forgiving about this and renders item 4 as plain text, making the error completely invisible to anyone not running the validator.

Why the Browser Hides It

This is precisely why the validator exists. Browsers apply error-recovery rules to invalid HTML and render something reasonable — in this case, displaying the text from the <li> as an unstyled paragraph. The result is visually indistinguishable from correct markup. The structural error is only revealed by a tool that checks against the spec, not against what a browser happens to accept.

The Fix: One Tag Change

Since the <li> was already using style="list-style:none" to suppress the bullet (it was being used as a plain paragraph, not a genuine list item), the fix was to call it what it actually was:

<!-- Before: invalid -->
<li style="list-style:none; margin-top:0.5rem;">4. Save the bookmark.</li>

<!-- After: valid -->
<p style="margin-top:0.5rem;">4. Save the bookmark.</p>

The visual result is identical. The validator is now satisfied.

The Rule

<li> is only valid as a direct child of <ol>, <ul>, or <menu>. If you need to insert a block element (a <div>, a <pre>, a Copy button) between list items, you have two correct options:

  1. Close and reopen the list. End the <ol>, insert the block element, start a new <ol> with start="4" to continue the numbering. The numbering continues visually; the structure is valid.
  2. Use a <p> instead. If the “item” after the break does not need to be part of the list semantically — as in this case, where it was already styled as plain text — a <p> is both simpler and correct.

A <li style="list-style:none"> using CSS to disguise itself as a paragraph is a semantic tell: if you are removing the one visual property that makes it a list item, it probably should not be a list item.

The Broader Lesson

This error was introduced by AI-generated HTML, caught immediately by running the W3C validator, and fixed in under a minute. The validator log read: “Element ‘li’ not allowed as child of element ‘div’ in this context. (Suppressing further errors from this subtree.)” The “suppressing further errors” note is important: when a structural parent is invalid, the validator stops reporting errors from inside it, since they may all be downstream consequences of the one root problem. One error in the log can represent a cluster of structural violations. Fix the parent first, then re-validate.

Context: Ask Copilot Entry #033 (the page where the fix was applied).

DWR
08/01/2026

DevTools — Stripping All CSS Without an Extension: The Console Command and the Bookmarklet

Read more…

The Web Developer Toolbar’s legendary “Disable All Styles” feature — the one every CS teacher prizes — is, under the hood, a single line of JavaScript. That means anyone can run it instantly in the browser console, or package it as a bookmarklet that works on any page in any browser with one click. No extension. No admin permission. No installation.

Method 1: The Console Command

Open DevTools (F12), click the Console tab, paste this, and press Enter:

document.querySelectorAll('link[rel="stylesheet"], style').forEach(el => el.remove());

Every <link rel="stylesheet"> and every <style> block is removed from the DOM. The page reverts to naked HTML. Reload to restore. This is exactly what TNT’s Strip CSS demo does on the home page — the same two element types documented in Entry #22.

Method 2: The Bookmarklet (One Click, Any Page, Any Browser)

A bookmarklet is a browser bookmark whose URL is JavaScript instead of a web address. Create a new bookmark, give it a name like Strip CSS, and paste the following as the URL:

javascript:(function(){document.querySelectorAll('link[rel="stylesheet"],style').forEach(function(e){e.remove();});})();

Navigate to any page, click the bookmark, and the styles disappear. Works in Chrome, Firefox, Edge, and Safari. School extension restrictions do not apply — bookmarklets are just bookmarks. The full entry in Ask Copilot includes a Copy button to grab the bookmarklet URL safely.

Why This Is a Eureka

Chris Pederick’s Web Developer Toolbar has been the professional go-to for this feature since 2003. It is genuinely excellent. But its disable-styles feature is the bookmarklet above wrapped in an extension button. Understanding what the tool does — and building a portable version of it from first principles — is exactly the kind of thinking TNT is training toward.

There are also two additional methods worth knowing: Firefox’s built-in Style Editor panel (per-stylesheet toggle buttons, no JavaScript required) and Chrome’s Rendering panel → Emulate print media (simplified layout view without a full strip). All four methods are compared in the companion Ask Copilot entry.

What the Strip Does NOT Remove

Inline style="" attributes on individual elements survive — because they are HTML attributes, not stylesheet documents. This is the same lesson as Entry #22. Run the bookmarklet on TNT’s home page and watch the hero icon: it stays flipped (facing right) even after stripping, because the flip lives in an inline style attribute. See Ask Copilot Entry #028 for why that is both a constraint and a deliberate teaching moment.

Full coverage including all four methods, a comparison table, and a Copy button for the bookmarklet: Ask Copilot Entry #033.

Eureka
08/01/2026

Man of Steel X-Ray — Hero Background Invisible: Placeholder Filename + Broken background Shorthand Syntax

Read more…

The hero section of the Man of Steel clip page was rendering as a solid dark color with no background image. The CSS was present. The file existed. Nothing in the browser showed an obvious error. Two separate problems were layered on top of each other.

Problem 1: A Placeholder Filename Left Behind

When the page was first built, no actual hero image had been provided yet. A placeholder filename was used — noun-xray.png — sized as a small 180px centered icon, not as a full-bleed hero background. Later, when the real image (Xray18.webp) was dropped into the images folder and its filename was substituted into the CSS, the original sizing value of 180px was left in place. The image was loading — at 180px wide, centered, surrounded by the dark background-color fallback on all sides. It was there. It was just tiny and invisible against the dark overlay.

The original placeholder intent and the new full-bleed intent were using completely different CSS size values, and the substitution only changed the filename, not the size. This is a common placeholder trap: a filler value serves one purpose during development and silently survives into the real implementation unchanged.

Problem 2: Invalid background Shorthand Syntax

To switch from the small icon to a full hero, the attempt was to simply add cover to the existing value:

/* What was written — broken */
url('../images/Xray18.webp') center / 180px no-repeat cover

This is invalid CSS. The browser silently ignores an invalid background shorthand layer rather than throwing an error — so nothing changes on screen, and no console message explains why. The DevTools Styles panel is the only reliable way to catch this: an invalid shorthand declaration either disappears entirely from the computed styles or is flagged with a yellow warning triangle next to the rule.

The CSS Background Shorthand Syntax Rule

The background shorthand accepts multiple values in a specific order. When both position and size are specified, they must be separated by a /, and size must come immediately after the slash — nothing can appear between them:

background: [color] [image] [position] / [size] [repeat] [attachment];
Written valueResultWhy
center / cover no-repeat ✓ Correct cover is immediately after / — parsed as background-size
center / 180px no-repeat cover ✗ Invalid 180px is parsed as size; cover is stranded after no-repeat with no valid slot — whole declaration ignored
center no-repeat / cover ✗ Invalid no-repeat must come after the position / size pair, not before it
center / contain no-repeat ✓ Correct Same rule: size keyword immediately follows /

Valid background-size values that can follow the /: cover, contain, auto, a length (300px), a percentage (50%), or two values (100% auto).

Multiple Backgrounds (Gradient + Image)

This page also uses a gradient layered over the image — a common pattern for readable hero text. Multiple background layers are comma-separated; the first layer listed paints on top:

background:
    linear-gradient(135deg, rgba(4,12,28,0.92) 0%, rgba(24,58,100,0.70) 100%),
    url('../images/Xray18.webp') center / cover no-repeat;

Each layer is parsed independently. The gradient has no position / size syntax, so it is straightforward. The image layer uses the corrected center / cover no-repeat form. The background-color: #04080e on a separate line acts as the ultimate fallback if both layers fail to load.

How DevTools Surfaces This Class of Error

When a background shorthand is invalid, the browser’s behavior is silent rejection — no console error, no page error, just the declaration having no effect. The tell is in DevTools:

  1. Open DevTools → Elements panel
  2. Click the hero <section>
  3. In the Styles panel, find the background rule
  4. A yellow triangle icon next to the rule means the browser rejected the value
  5. The Computed tab shows what is being applied — if background-size shows auto instead of cover, the size value was not parsed correctly

This is a practical use of the VERIFY framework’s A (Inspect appearance) and V (Validate) layers: the visual symptom pointed to CSS, the Styles panel confirmed the rule was being rejected, and the fix was in the syntax rather than the file reference.

The Fix

/* Before — invalid: 180px is the size, cover is stranded */
url('../images/Xray18.webp') center / 180px no-repeat cover

/* After — correct: cover immediately follows the slash */
url('../images/Xray18.webp') center / cover no-repeat

The two-part rule: (1) when copying a placeholder CSS value into production, always audit all properties set by that placeholder — not just the filename. (2) In the background shorthand, position / size must be adjacent, size immediately after the slash, repeat after that.

DWR
07/31/2026

Home Page — Inline style="" Attributes Survive Strip CSS — and What That Teaches About the Three Places Styles Can Live

Read more…

After Ask Copilot Entry #027 added a horizontal flip to the hero icon using an inline style="transform:scaleX(-1)" attribute, the icon kept facing right after clicking the Strip CSS demo — even though it should have reverted. The strip mechanism does exactly what it says: it strips stylesheets. An inline attribute is not a stylesheet. This apparent misbehaviour is a perfect teaching moment about the three places styles can live.

The Three Places Styles Can Live

LocationExampleRemoved by Strip CSS?
External stylesheet <link rel="stylesheet" href="tnt-home.css"> ✓ Yes — the <link> element is removed
<style> block <style>.x{color:red;}</style> ✓ Yes — the <style> element is removed
Inline style="" attribute <img style="transform:scaleX(-1);"> ✗ No — it is an HTML attribute, not a CSS document

Why Inline Styles Survive

Strip CSS removes <link> and <style> elements from the DOM. A style="" attribute is a property of an HTML element — part of the markup itself, not a CSS document. It lives alongside alt="" and aria-hidden="true"; removing stylesheet files does not affect it. This is also why inline styles win every specificity fight: they are not in the cascade at all. A stylesheet rule cannot override style="transform:scaleX(-1)" without !important because one is HTML and the other is CSS, and HTML property wins before the cascade begins.

The Fix: Move the Flip to a CSS Class

Moving transform: scaleX(-1) to a .hero-title-icon class in tnt-home.css makes the directional flip a stylesheet-level decision that Strip CSS can remove. The inline attribute retains only layout properties (size, spacing, alignment) that are structural and not the subject of the demo:

/* tnt-home.css — disappears with Strip CSS */
.hero-title-icon { transform: scaleX(-1); }
<img class="hero-title-icon"
     style="height:1.1em; vertical-align:middle; margin-right:0.35rem;">

The result is a two-state live demo:

  1. CSS intact: icon faces right — toward the heading, applying the directional cue principle from Entry #027
  2. CSS stripped: icon faces left — its natural, unmodified orientation

Keeping sizing inline (rather than moving everything to CSS) is intentional: it isolates the flip as the single observed change, which is the lesson. Full discussion in Ask Copilot Entry #028.

Eureka
07/24/2026

Compute πspockPiclip.htmlspockPIclip.html: URLs Are Case-Sensitive on Live Servers (and No, Scotty Can’t Just Beam a Fix)

Read more…

The link bar at the bottom of computePi.html contained this: <a href="../movie_clips/spockPiclip.html">. The actual file on the server is named spockPIclip.html — capital PI, not lowercase Pi. One letter, one case difference, one missing page on a live server. The Enterprise would have called it a Class-1 Navigation Error.

On the local Windows development machine, it worked perfectly. Windows file paths are case-insensitive, so spockPiclip.html and spockPIclip.html are the same file as far as Windows is concerned. The error was completely invisible during development — just like the Romulans before they decloak.

On the live Linux server, the path movie_clips/spockPiclip.html did not match movie_clips/spockPIclip.html. Linux is case-sensitive. The file was simply not found. Fortunately, TNT’s siteUnderConstruction.html fallback intercepted the 404 gracefully, so visitors saw a construction page rather than a raw browser error. We did not look completely incompetent. Mostly competent. Spock would note the distinction is not purely academic.

The Fix

One character change in computePi.html:

<!-- Wrong (lowercase i) — 404 on Linux server -->
<a href="../movie_clips/spockPiclip.html">

<!-- Correct (uppercase PI — matches the actual filename) -->
<a href="../movie_clips/spockPIclip.html">

Case Sensitivity: A Complete Starfleet Briefing

This incident is a good excuse to document exactly what is and is not case-sensitive in web development. Stardate 07/24/2026. Pay attention, cadets.

ItemCase-Sensitive?Notes
File names on Linux servers ✓ YES index.htmlIndex.html. All production web servers are Linux. Your Mac might forgive you; the server will not.
File names on Windows ✗ NO Windows NTFS is case-insensitive but case-preserving. It remembers the case you used, but it won’t enforce it. This is why the bug was invisible locally.
URL path & query (everything after the domain) ✓ YES on Linux /movie_clips/spockPIclip.html/movie_clips/spockPiclip.html on a Linux server. The path maps directly to the filesystem.
Domain name ✗ NO TECHNOVICETOOLS.COM = technovicetools.com. The DNS system is case-insensitive. The domain is safe.
URL scheme (http://, https://) ✗ NO HTTPS:// = https://. The scheme is case-insensitive per the URI specification.
HTML tag names ✗ NO <DIV> = <div>. HTML5 is case-insensitive for tags. Convention is lowercase.
HTML attribute names ✗ NO HREF = href. HTML attribute names are case-insensitive. Values, however, depend on the context.
HTML attribute values (id, class) ✓ YES id="myDiv"id="MyDiv". Element IDs and class names are case-sensitive. A CSS rule #myDiv will not match an element with id="MyDiv".
CSS property names ✗ NO COLOR: red = color: red. CSS property names are case-insensitive. Convention is lowercase.
CSS selectors (class & ID names) ✓ YES .myClass.MyClass. CSS class and ID selectors are case-sensitive because they reference HTML attribute values, which are also case-sensitive.
JavaScript variable & function names ✓ YES computeLeibnizComputeLeibniz. JavaScript is fully case-sensitive. This is where the most runtime errors live.
Image src attributes ✓ YES on Linux src="Images/logo.png" will 404 if the folder is named images/ on a Linux server. The path is resolved via the filesystem.
CSS url() paths ✓ YES on Linux url('../Images/hero.jpg') will fail if the folder is images/. Same filesystem resolution rule as src.
localStorage keys ✓ YES localStorage.getItem('myKey')localStorage.getItem('MyKey'). They are two separate storage entries.

The Starfleet Rule

Assume everything is case-sensitive. Always. Windows’ forgiveness is a trap — it produces developers who are surprised when their perfectly working local app 404s on the live server. Adopt the Linux mindset during development: be exact, be consistent, be lowercase. Pick a naming convention (TNT uses lowercase for folder names, CamelCase for file names) and never deviate.

And always test on the live server before declaring victory. Or as Spock would put it: “A hypothesis that has only been tested in the laboratory has not been confirmed, Captain.”

How siteUnderConstruction.html Saved Face

TNT’s server configuration routes unknown or 404 paths to siteUnderConstruction.html rather than returning a raw browser error page. This is server-level error handling: visitors see a friendly placeholder rather than a jarring “Not Found” response. It does not fix the broken link, but it significantly reduces the visible damage until the fix is deployed. Credit where credit is due: the failsafe held. The LCARS system would be pleased.

DWR
07/18/2026

speeches.html — p5.js touchStarted Returns false Globally: One Line That Silently Killed the Navbar Burger on Touch Devices

Read more…

After applying the Bootstrap SRI hash fix from Entry #19, every SPARK Speeches page had a working burger menu — except speeches.html. A hard refresh confirmed the fix was in place: no integrity attribute, one clean Bootstrap JS tag. The dragon sketch worked. The page validated. But on a touch device (or a browser in mobile-emulation mode), the navbar burger did nothing.

Root Cause: return false in p5.js Is Global

The dragon sketch included a single line added as a scroll-prevention measure for mobile:

p.touchStarted = function() { return false; };

In p5.js, returning false from a touch callback calls event.preventDefault() on the touch event. That is documented and intentional. What is easy to miss: the touchStarted callback fires for every touchstart event on the entire page, not just touches on the canvas.

When a user tapped the navbar burger button on a touch device:

  1. A touchstart event fired on the burger button
  2. p5.js’s touch handler intercepted it at the document level
  3. p5.js called event.preventDefault()
  4. The browser’s touch-to-click synthesis was cancelled
  5. Bootstrap’s collapse plugin (which listens for click) never saw it
  6. The burger appeared to do nothing

The other six SPARK Speeches pages had no p5.js sketch, so their taps went through unmodified and the burger worked correctly — making this one look like a missed fix when it was actually a different bug entirely.

Why It Was Hard to Find

  • The sketch worked. The Bootstrap fix was confirmed applied. The page validated. Nothing looked wrong.
  • The symptom — burger menu not responding — was the exact same symptom as Entry #19, making the natural assumption “the previous fix didn’t take.”
  • The problem is completely invisible on a desktop with a mouse. touchStarted never fires for mouse clicks, so desktop testing showed nothing wrong.

The Fix

Scope return false to canvas touches only, using e.target:

// Before — blocks ALL touchstart events on the page:
p.touchStarted = function() { return false; };

// After — blocks default only for touches on the canvas itself:
p.touchStarted = function(e) {
  if (e && e.target === p.canvas) { return false; }
};

p.canvas in p5.js instance mode is the raw canvas DOM element. Comparing e.target to it ensures preventDefault() only fires for touches directly on the canvas — exactly where scroll and zoom suppression belongs. Taps anywhere else on the page (navbar, buttons, links) pass through unmodified.

The Rule

In p5.js, return false in any event handler (touchStarted, mouseClicked, keyPressed, etc.) calls event.preventDefault() globally — for all matching events on the page, not just those targeting the canvas. Whenever a sketch is embedded in a page with other interactive elements (navbars, buttons, forms), always scope these blocks with an e.target guard:

p.touchStarted = function(e) {
  if (e && e.target === p.canvas) {
    // canvas-only prevention (scroll, zoom, etc.)
    return false;
  }
  // all other touches pass through unblocked
};

Fix applied in speeches.html. The Bootstrap SRI hash context is in Entry #19.

DWR
07/18/2026

SPARK Speeches — Bootstrap SRI Hash Mismatch: Wrong integrity Attribute Silently Kills All Bootstrap JavaScript

Read more…

Every page in the SparkSpeech2026-05-05-Stg1/ folder had a working navbar burger button that produced no effect when clicked. The button was there, the Bootstrap markup was correct, the data-bs-toggle="collapse" and data-bs-target="#mainNav" attributes matched perfectly — but nothing happened. The burger menu was cosmetically correct and functionally dead.

Root Cause: Subresource Integrity (SRI) Check Failure

The Bootstrap JS <script> tag in these pages carried an integrity attribute declaring a specific SHA-384 hash of the expected file content:

integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmStTJaEBr5r+eLWLGNNwj5pEQm"

When a browser fetches a resource that has an integrity attribute, it computes the SHA-384 hash of the downloaded bytes and compares it against the declared value. If they differ, the browser refuses to execute the script entirely — no error on the page, no console message that is obvious to a developer scanning for problems, just silence. The script is fetched, hashed, found non-matching, and discarded.

This hash does not match the Bootstrap 5.3.3 bundle served by jsDelivr. The correct official hash for the Bootstrap 5.3.3 bundle is:

sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I

Because Bootstrap JS never loaded, every feature it powers was silently dead: navbar collapse (the burger menu), modals, offcanvas panels, dropdowns, accordions — the entire interactive layer.

Why the CSS Looked Fine

The Bootstrap CSS link also carried an integrity attribute, but its hash happened to be correct for the jsDelivr-served file. CSS loaded and applied normally, so every page looked perfectly styled — it just didn’t do anything interactive. This made the bug harder to spot: a broken page looks broken; a fully-styled page with dead JavaScript looks fine until you try to use it.

The Fix

Remove integrity and crossorigin from both the Bootstrap CSS <link> and the Bootstrap JS <script> tags, consistent with the standard practice used throughout the rest of the TNT site:

<!-- Before (broken) -->
<script
  src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
  integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmStTJaEBr5r+eLWLGNNwj5pEQm"
  crossorigin="anonymous">
</script>

<!-- After (correct) -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Applied across all 7 pages in SparkSpeech2026-05-05-Stg1/: sparkCover.html, speeches.html, sparkTools.html, showcaseDragonSourceCode.html, learnMoreAboutSPARK.html, gettingStarted.html, and aboutSparkSpeeches.html.

How to Avoid This

SRI hashes are a legitimate security feature — they prevent a CDN from serving a tampered file. But they require that the hash in the HTML exactly matches every byte of the CDN-served file. Different CDN providers, different distribution versions, or even minor tooling differences can produce files with different hashes for the same logical version. When the hash is wrong, the failure is completely silent.

ApproachTrade-offTNT Practice
No integrity attribute Trusts the CDN; slightly reduced supply-chain security ✓ Standard for all TNT pages
integrity with a verified hash Maximum security; breaks silently if hash drifts Use only when hash is confirmed against the live CDN file

The rule: Never copy an integrity hash from an AI-generated snippet, a tutorial, or another project without verifying it against the actual file the CDN will serve. If you cannot verify it, omit the attribute. A missing hash is a deliberate choice; a wrong hash is an invisible breakage that may not be noticed until a user reports that buttons do nothing.

Live context: SPARK Speeches home. After this fix was applied, speeches.html’s burger remained broken on touch devices due to a completely separate p5.js interaction — see Entry #20.

DWR
07/18/2026

Copilot Comedian<br> and \n Don’t Work in .textContent — and Why That’s by Design

Read more…

The Copilot Comedian prompt text “Press the button… if you dare.” was a single line. Adding a <br> to split it across two lines produced no effect whatsoever — the tag appeared literally on screen. Switching to \n also did nothing. The fix was a one-word change to the JavaScript property being used, but the reason it failed matters more than the fix.

The Two Properties and What They Do

PropertyTreats content as…HTML tagsResult of <br>
.textContent Plain text Escaped and displayed literally &lt;br&gt; appears on screen
.innerHTML HTML markup Parsed by the browser Renders as a real line break

When you set .textContent, the browser takes your string at face value and displays it exactly as written — angle brackets and all. There is no warning, no error. The tag just appears as text. That silence is what makes this a DWR: the failure is completely invisible.

The fix was one word:

/* Before — 
appears literally on screen */ jokeText.textContent = "Press the button\u2026 if you dare."; /* After —
renders as a real line break */ jokeText.innerHTML = "Press the button\u2026<br>if you dare.";

Why Does \n Also Fail?

\n is a genuine newline character in the JavaScript string — it is not the problem. The problem is HTML. HTML collapses all whitespace (spaces, tabs, newlines) into a single space when rendering text inside a <p> element. The newline arrives in the DOM correctly; the browser then ignores it when painting the page. Only a real HTML element (<br>, <p>, white-space: pre in CSS) can force a visual line break.

But Isn’t .textContent Preferred?

Yes — and for a specific reason: security. When you set .innerHTML, the browser parses the string as HTML. If that string contains user-supplied data, a malicious user could inject a <script> tag or an event-handler attribute (onerror="stealCookies()") and execute arbitrary JavaScript. This is called Cross-Site Scripting (XSS) and it is OWASP’s top web vulnerability.

.textContent is immune to XSS because it never parses HTML — every character is displayed as-is. That is why it is the safe default for any string that originates from user input, a database, or an API response.

The rule of thumb:

Source of the stringUseWhy
User input, database, API .textContent Prevents XSS; HTML tags shown literally (harmless)
Developer-controlled literal string in source code .innerHTML (safe) No user data; HTML markup is intentional

In the Copilot Comedian, both strings are developer-written literals baked into the JavaScript source file. No user input ever reaches the DOM. Using .innerHTML for the prompt text is safe. The joke text from the jokes[] array is also developer-written, so its .textContent assignment in displayJoke() is correct for a different reason: the jokes are plain text, contain no markup, and .textContent is the simpler, more precise property when you have no HTML to parse.

Was the Two-Line Split Worth the Change?

Yes. The app is a comedy stage and delivery is part of the joke. “Press the button…” is the setup; “if you dare.” is the punchline. A line break between them creates the visual equivalent of a pause — the same beat a stand-up comedian uses before landing a line. In a straight utility app the change would be cosmetic noise; on a comedy stage it is theatrical craft. The context justified the technique.

Quick Reference

  • .textContent = "hello <br> world" → displays hello <br> world literally. Safe for user data.
  • .innerHTML = "hello <br> world" → renders a real line break. Safe only for developer-controlled strings.
  • \n in a string → ignored by the HTML renderer inside block elements. Use <br> via .innerHTML or white-space: pre-wrap in CSS instead.
DWR
07/15/2026

AI Sudoku GeneratorDate.now(): A 13-Digit Number That Is Also a Timestamp, a Unique ID, and a History Lesson

Read more…

The AI Puzzle Generator assigns each saved puzzle an ID like easy-1784168082196. A student noticed the 13-digit suffix and asked: what is that number, and how is it generated? The answer connects a one-line JavaScript call to the entire history of modern computing.

What It Is

The number comes from Date.now(), one of the simplest functions in JavaScript:

var id = difficulty + '-' + Date.now();
// produces e.g.  'easy-1784168082196'

Date.now() returns the number of milliseconds elapsed since January 1, 1970, 00:00:00 UTC — the “Unix epoch.” As of mid-2026, that count is approximately 1.784 trillion milliseconds: a 13-digit number.

Why 13 Digits in 2026?

MilestoneDateDigits
Unix seconds cross 109September 9, 200110 (sec) • 13 (ms)
TodayJuly 15, 202613 digits
JS ms cross 1013November 20, 2286becomes 14

Dual Purpose: Unique ID + Readable Timestamp

The timestamp suffix serves two roles simultaneously:

  • Guaranteed uniqueness: two puzzles generated even 1 ms apart get different IDs — no database counter, no UUID library required.
  • Decodable creation date: divide by 1,000 to get Unix seconds, then paste into any epoch converter: 1784168082196 ÷ 1000 = 1784168082 → July 15, 2026.

The Unix Epoch — A Brief History

January 1, 1970 was chosen by the early Unix developers at Bell Labs in the late 1960s as a convenient round date to count from. It has since been adopted by every major OS and programming language. Date.now() in JavaScript is the direct descendant of C’s time() system call — same epoch, same idea, 1,000× finer resolution. When you call Date.now(), you are using an abstraction that has been continuous since the first Unix systems in the early 1970s.

The Design Pattern

The difficulty-timestamp format is a practical pattern worth remembering for any situation that needs lightweight unique IDs:

  • Human-readable prefix for context
  • Timestamp suffix for uniqueness and creation date
  • Zero dependencies — no library, no server, no counter
  • Sorts chronologically by default

Documented in the Sudoku S.P.A.R.K. Chat Log.

Eureka
07/08/2026

Ambiguous Message PHP — POST-Refresh Trap: The Browser “Resubmit Form Data?” Alert

Read more…

The app was functionally complete and about to launch. A final pre-launch walkthrough revealed a boundary condition that happy-path testing had never reached: after submitting the correct password, pressing F5 or the browser Refresh button produced a browser-native alert — “Are you sure you want to resubmit the form data?” Dismissing it left the app in an ambiguous half-state with no clean path back to the original puzzle.

Why the Browser Does This

When a form uses method="POST", the browser records the last HTTP action as a POST. Refreshing would re-send that POST — which could charge a card twice, post a comment twice, or place a duplicate order. The browser cannot tell the difference between a harmless password check and a destructive transaction, so it warns every time. This is a security feature, not a PHP bug.

The Formal Solution: Post-Redirect-Get (PRG)

The standard fix for production forms:

  1. User submits form → browser sends POST
  2. Server processes the POST
  3. Server responds with HTTP 303 redirect to the same URL
  4. Browser follows redirect with a clean GET
  5. Server responds to GET with the result page

Because the final load is a GET, refreshing replays the GET — no resubmission warning. PRG requires $_SESSION to carry state across the redirect, which is correct for production apps:

session_start();
if ($guess === $correctPassword) {
    $_SESSION['passwordCorrect'] = true;
    header('Location: ambigMsgPHPIndex.php');
    exit; // IMPORTANT: always exit() after header('Location: ...')
}

The Practical Fix: a “Start Over” Anchor Button

An <a> element always sends a GET request — never POST. For this educational demo, a simple anchor link below the reveal gives users a clean reset path without sessions or complexity:

<!-- Anchor = GET. No POST data. No browser alert. -->
<a href="ambigMsgPHPIndex.php" class="btn btn-sm">
    <i class="fas fa-rotate-left"></i> Start Over
</a>

Clicking it loads a fresh page, PHP resets $passwordCorrect = false, and the original puzzle reappears. Zero sessions, zero complexity.

The Lesson: Test Boundary Conditions Before Launch

Main-path testing covers: wrong password → error; correct password → reveal. Boundary conditions are the edges that main-path testing skips. For any PHP form-based app, before going live:

  • Submit correctly, then F5/Refresh — POST-refresh trap
  • Submit correctly, then press Back — can the user return cleanly?
  • Submit with an empty field — what does trim(“”) give PHP?
  • Submit the correct answer twice — does anything break?

This boundary was found during a pre-launch walkthrough — the right moment to find it, and a reminder that walkthroughs should include deliberate edge-case exploration, not just happy-path confirmation. The Start Over button, this DWR entry, and the chatlog note all exist because one final test found one edge case. That is the correct order of events.

Full context in the Ambiguous Message PHP chat log.

DWR
07/07/2026

Ambiguous Message — Auto-Focusing a Modal Input: Why autofocus Fails and How shown.bs.modal Solves It

Read more…

When a Bootstrap modal opens, the cursor should land in its first input automatically. The HTML autofocus attribute seems like the obvious solution — but it silently does nothing on modal fields.

Why autofocus Fails on Modal Inputs

Bootstrap modals start as display: none. The browser processes autofocus when the element is first encountered at page load — when the modal is still invisible. A hidden element cannot receive focus; the browser attempts it, fails silently, and moves on. By the time the user opens the modal, the autofocus window is long past.

Bootstrap’s Modal Lifecycle Events

Bootstrap fires four events on the modal element during its lifecycle:

EventWhen it fires
show.bs.modalWhen show() is called — before animation
shown.bs.modalAfter animation completes — modal fully visible
hide.bs.modalWhen hide() is called — before animation
hidden.bs.modalAfter animation completes — modal fully hidden

shown.bs.modal fires after the CSS transition and the modal is fully in the viewport. At that point the input is visible, focusable, and ready:

document.getElementById("myModal").addEventListener("shown.bs.modal", function() {
    passwordInput.focus();
});

The General Rule

Any side effect that requires a modal to be in its final visible state belongs in a shown.bs.modal listener. The same pattern applies to: selecting existing text, initialising a third-party widget, or starting an animation that depends on the modal being visible.

The “before” events (show, hide) are for things that must happen before the animation — like cancelling the open or injecting dynamic content. The “after” events (shown, hidden) are for things that require the final state to be in place.

The UX principle: a form that opens specifically for user input should immediately accept that input. An extra click to enter a field the user just asked to see is friction that benefits nobody. Don’t hack off your users.

Applied in the Ambiguous Message.

Eureka
07/06/2026

ponderPhrasesSpark.js — Mojibake: When Smart Quotes and Em-Dashes Turn Into Garbage

Read more…

After PowerShell copied ponderPhrases.js to ponderPhrasesSpark.js, strings like wouldn’t displayed as wouldn’t, Soup— appeared as Soupâ€", and passé showed as passé. This is a classic encoding problem called mojibake — Japanese for “unintelligible characters.”

What Is Mojibake?

Some characters — smart quotes, em-dashes, accented letters — require more than one byte in UTF-8. A right single quotation mark (U+2019) is stored as three bytes: E2 80 99. When a program reads that file using the wrong encoding (Windows-1252 instead of UTF-8), each byte gets decoded independently:

ByteWindows-1252 char
E2â (U+00E2)
80€ (U+20AC)
99™ (U+2122)

Three bytes that represent one character get decoded as three separate characters: ’. The three mojibake sequences in this file and their correct replacements:

Bad sequenceThird byteShould beName
’U+2122 (™) U+2019Right single quotation mark
—U+201D (”) U+2014Em dash
éU+00A9 (©)é U+00E9e with acute accent

Why Did the Fix Seem So Hard? Three Obstacles

Obstacle 1: You cannot reliably type or paste the bad characters. The mojibake sequence ’ looks like three printable characters in an editor. But when you type those same characters into a terminal command, your keyboard and terminal may encode them differently — meaning the typed string and the file string have different codepoints, and .Replace() finds nothing. The only reliable way is to identify the exact Unicode codepoints by inspection:

$m = [regex]::Match($content, 'â€.')
$m.Value.ToCharArray() | ForEach-Object { "U+{0:X4}" -f [int]$_ }
# outputs: U+00E2  U+20AC  U+2122

Then build the search string from those codepoints explicitly, so there is no ambiguity about what you are searching for:

$badApos = [char]0x00E2 + [char]0x20AC + [char]0x2122

Obstacle 2: The apostrophe and em-dash look almost identical but use different third bytes. Both start with U+00E2 + U+20AC, but the third byte differs: apostrophe ends in U+2122 (™) and em-dash ends in U+201D (”). They must be searched and replaced separately, and the distinction is invisible to the naked eye when viewing the file.

Obstacle 3: PowerShell’s .Replace() overload resolution. String.Replace() in .NET has two overloads: Replace(char, char) and Replace(string, string). When the replacement argument is a bare [char] expression, PowerShell resolves to the Replace(char, char) overload — which then throws “String must be exactly one character long” because the search string is three characters. The fix is to explicitly cast the replacement to a string first:

# Fails — PowerShell picks Replace(char, char) overload
$c = $c.Replace($badApos, [char]0x2019)

# Works — explicitly forces Replace(string, string) overload
$goodApos = [string][char]0x2019
$c = $c.Replace($badApos, $goodApos)

Why It Happened

The original ponderPhrases.js was saved at some point by a tool that read its UTF-8 bytes as Windows-1252 and re-saved, permanently baking the mojibake into the file as plain text characters. When PowerShell copied it to ponderPhrasesSpark.js, it faithfully reproduced every character — garbage in, garbage out. The file ponderPhrasesSpark0.js is preserved as a before/after reference showing the unfixed state.

Prevention and Detection

  • Check encoding before copying. In VS Code, the file encoding shows in the bottom-right status bar. If it says Windows-1252 or ANSI, convert to UTF-8 first.
  • Scan before shipping. Any †sequence in a data file is almost certainly mojibake. Run a quick scan: [regex]::Matches($content, 'â€|Ã.').Count
  • Use explicit codepoints in fix scripts. Never copy-paste the bad characters as literals in a fix script. Build them from [char]0xXXXX values to eliminate encoding ambiguity.

This incident was documented during the Pondering SPARK development session.

DWR
07/06/2026

Explore! — Two Roles, One Card: Why Category Tile Lists Need an Editorial Cap

Read more…

When the Pondering SPARK Edition was added to the S.P.A.R.K. category tile on explore.html, the featured app list grew to six items — the most in any category. That prompted a question: should a JavaScript variable (tileAppListMax) enforce the cap automatically, or is editorial discipline the right tool?

The Two-Role Architecture

Every category in the grid serves two different students with two different goals:

  • The tile card serves discovery — a student scanning the grid should be able to identify the category in a glance and pick 1–3 apps that represent it well. The card is a signpost, not a catalogue.
  • The offcanvas panel serves browsing — a student who already knows they want this category opens the offcanvas and sees the complete, scrollable inventory.

When a tile list grows to six or more items, it is doing the offcanvas’s job badly — too long to scan, too short to be complete.

Why Not a JS Variable?

A tileAppListMax variable enforces quantity but not quality. Auto-hiding items beyond index 3 would hide the wrong three if items weren’t added in priority order. It also renders HTML that is immediately hidden — wasteful and surprising to the next developer reading the file. The existing comment in the source already says “2–3 max”; the problem was the convention wasn’t followed, not that it wasn’t enforced.

The Decision

Editorial cap at 3, curated. The three items on any card should be the most representative of that category — chosen intentionally, not just the last three added. When a 4th app ships, decide which 3 to feature; don’t just append. Everything goes in the offcanvas regardless.

For the S.P.A.R.K. card, the three kept are Yahtzee SPARK, Magic 8 Ball SPARK, and Pondering SPARK — three interactive apps that each illustrate a different facet of the method. The tutorials (Git Workflow, Font Awesome, Purple People Eater) remain fully accessible in the offcanvas.

The Rule

Category 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.

This discussion was triggered during the Pondering SPARK development session.

Eureka
07/06/2026

Legacy Archive — Adding the Legacy Archive Banner to Sub-Directory Pages

Read more…

The legacyBanner.js script injects a persistent amber notification bar at the very top of every _LegacyTNT/ page, telling visitors they are browsing the historical archive — not the current TNT site. It includes a “Return to Current TNT Site” link and a dismiss button that collapses the banner to a small tab instead of hiding it entirely. It is self-contained (all styles are inline), so it is immune to the legacy stylesheet, and it is loaded with a single <script> tag placed just before </body>.

Standard Usage — One Level Deep

The script hard-codes CURRENT_SITE_URL = "../index.html". For pages that sit directly inside _LegacyTNT/ (one level deep), one ../ step up lands on the current TNT root, so the return link works perfectly. Adding the banner is one line:

<script src="scripts/legacyBanner.js"></script>

See it on the Legacy Archive index page.

The Problem — Two Levels Deep

When we added the banner to SteppedCircles, which lives two levels deep (_LegacyTNT/SteppedCircles.../index.html), the relative URL "../index.html" resolved to _LegacyTNT/index.html — the legacy index — rather than the current TNT root. “Return to Current TNT Site” sent visitors to the wrong page.

The Solution — a Page-Level URL Override

Duplicating the 300-line script just to change one URL felt wrong. Instead, we gave legacyBanner.js a one-line change that reads an optional global variable first:

var CURRENT_SITE_URL = window.tntLegacyReturnURL || "../index.html";

When window.tntLegacyReturnURL is not set, the script falls back to the original default — so every existing page keeps working without any change. A sub-directory page sets the override in a tiny inline <script> placed immediately before the banner script tag (order matters: the variable must exist before the script reads it):

<!-- Two levels deep: _LegacyTNT/SomeApp/index.html -->
<script>window.tntLegacyReturnURL = "../../index.html";</script>
<script src="../scripts/legacyBanner.js"></script>

For a page three levels deep, use "../../../index.html", and so on. Count how many folder levels separate the page from the TNT root and add one ../ per level.

Quick Reference

One level deep (directly inside _LegacyTNT/):

<script src="scripts/legacyBanner.js"></script>

Two levels deep (inside a sub-folder of _LegacyTNT/):

<script>window.tntLegacyReturnURL = "../../index.html";</script>
<script src="../scripts/legacyBanner.js"></script>

Live examples: Legacy Archive index (standard one-level use) and SteppedCircles (sub-folder use with URL override). The amber banner should appear at the top of both pages.

Eureka
07/05/2026

Brain Pondering — Reusing an Existing Deep-Link to Open a Specific Gallery Modal from Another Page

Read more…

The Brain Pondering app has a line in its “About the app” panel that reads: “Remember the power of font! — a plain link to the TNT Image Gallery. The goal was to make that link land directly on the Fonts Matter gallery entry with its modal already open, rather than dropping the user at the top of the gallery page and leaving them to hunt for it.

The Existing Infrastructure

Back in Entry 6 (06/06/2026), we built a ?open= query-parameter deep-link system into the gallery. On page load, init() reads the parameter, finds the card whose data-title matches, scrolls it into view, and programmatically clicks it so Bootstrap opens the modal:

const openTitle = new URLSearchParams(window.location.search).get('open');
if (openTitle) {
    const targetCard = Array.from(document.querySelectorAll('.gallery-item'))
        .find(card => card.dataset.title === openTitle);
    if (targetCard) {
        targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
        setTimeout(() => targetCard.click(), 250);
    }
}

The Fonts Matter card has data-title="Fonts Matter", so the deep-link URL becomes:

../tnt_image_gallery.html?open=Fonts%20Matter

The Change — One Attribute, Five Extra Characters

The only edit needed was adding the query string to the existing href:

<!-- Before -->
<a href="../tnt_image_gallery.html" target="_blank">power of font!</a>

<!-- After -->
<a href="../tnt_image_gallery.html?open=Fonts%20Matter" target="_blank">power of font!</a>

No JavaScript was written. No new HTML was added. The gallery page required zero modification. The entire solution was a query string appended to one href.

Why This Is a Eureka

The lesson here is about infrastructure paying dividends. When we built the ?open= system in Entry 6, we were solving a specific problem (the Loud Pictures movie page linking back to its gallery entry). But because we designed it generically — matching any data-title value — it immediately worked for every card in the gallery, from any page on the site, with no additional code.

This is a real-world example of the principle: build it right once, and future you will thank present you. A tiny upfront investment in a flexible, reusable URL convention turned a later problem into a one-line fix.

The Encoding Rule

Spaces in a query string must be encoded. The two common encodings are + and %20. In a query string value, both are valid, but %20 is safer and more explicit:

?open=Fonts%20Matter     <!-- correct: %20 encodes the space -->
?open=Fonts Matter       <!-- wrong: raw space breaks the URL -->

When in doubt, run the title through encodeURIComponent() in the browser console to get the correct encoding for any title string.

Live example: Open Brain Pondering, expand “About the app,” and click power of font! — the gallery opens directly to the Fonts Matter modal.

Eureka
06/09/2026

F4 Whole Team Template — Never Write </body> (or Any Closing Tag) Inside an HTML Comment

Read more…

A Copilot-generated comment at the bottom of the page included the literal closing tag </body> as a label. The result was strange: text that belonged inside the comment appeared as visible fragments at the very bottom of the rendered page.

What the Comment Looked Like

The comment block near the end of the file read something like:

<!-- END OF PAGE
     </body> and </html> close below
-->

Why It Broke

Same family of problem as Entry 8. The HTML5 parser watches for certain tag sequences everywhere — including inside comments. When the browser encountered </body> inside the comment, it treated it as the real end of the document body. Everything that followed — including the rest of the comment text and the actual closing tags below it — was pushed outside the body element. Browsers sometimes render that stray content as visible text at the very bottom of the page.

The symptom was odd comment fragments appearing as page text. The cause was a tag that was never meant to be parsed, being parsed anyway.

The Fix

Replace any closing tag text in comments with plain descriptive words:

<!-- END OF PAGE
     body end and html end close below
-->

Same intent. Zero parser side effects.

The Rule: Never write </body>, </html>, <script>, or any tag-like text inside an HTML comment. The HTML5 parser does not fully ignore comments — it still scans for certain tag sequences. Use plain descriptive words instead. This is the same root cause as Entry 8; closing tags are just as dangerous as opening ones.

DWR
06/09/2026

F4 Whole Team Template-- Inside an HTML Comment Breaks XML Validation

Read more…

The W3C validator flagged this as an info message — not a hard error, but it signals a real incompatibility worth knowing.

What Triggered It

A comment block used -- as a decorative section divider:

<!-- -- ABOUT EXTERNAL STYLESHEETS -------------------------------- -->

The message was: “The document is not mappable to XML 1.0 due to two consecutive hyphens in a comment.”

Why It Happens

The XML 1.0 specification reserves the sequence -- exclusively for comment delimiters: <!-- to open and --> to close. Using -- anywhere else inside a comment is technically illegal in XML, because a parser could misinterpret it as an attempt to close the comment. HTML5 browsers handle it gracefully, but the W3C validator checks against both HTML5 and XML 1.0 rules simultaneously.

The Fix

Replace -- section markers with any other character. We use ==:

<!-- == ABOUT EXTERNAL STYLESHEETS ================================ -->

Visually identical. Completely valid. One character change.

The Rule: Inside an HTML comment, -- may only appear as part of --> at the very end. Use =, ~, #, or any other character as a visual separator everywhere else.

DWR
06/09/2026

F4 Whole Team Template — Never Write <script> Inside an HTML Comment (Near a Real Script Tag)

Read more…

The Bootstrap modals and accordion on the page were completely silent — clicking cards and buttons did nothing. The browser console showed a cryptic error: “Uncaught SyntaxError: Unexpected token ‘,’” at a line number that didn’t even exist in our source file. That last detail was the key clue: when an error points to a non-existent line, the JavaScript parser has gone off the rails somewhere before that line and is trying to parse HTML as if it were code.

What We Had Written

The comment block just above the Bootstrap JS bundle tag read:

<!-- BOOTSTRAP JS BUNDLE - always last, just before </body>
     Without this <script>, the data-bs-* attributes won't work.
-->
<script src="...bootstrap.bundle.min.js"></script>

That looks completely harmless. It’s an HTML comment. Comments are ignored, right?

Why It Broke

HTML5 parsers have a special rule when they are scanning for script tags: they watch for the sequence <script anywhere in the document, even inside comments. When the parser saw <script> inside the comment, some browsers switched into script-parsing mode immediately — before reaching the closing -->. Once in script mode, the next thing the browser read was:

, the data-bs-* attributes won't work.
-->

That leading comma is valid HTML inside a comment, but it is completely illegal as the first token of a JavaScript statement. The parser threw “unexpected token ‘,’” and aborted the entire script block. Bootstrap never finished loading. Every component that depends on Bootstrap’s JavaScript — modals, accordions, dropdowns — was silently dead.

Why It Was Hard to Find

Three things made this especially tricky:

  • The error message (“unexpected token ‘,’”) pointed to a line number higher than our file’s total line count — meaning the parser had gone completely off-road.
  • The comment itself was valid HTML and visually looked fine.
  • The symptom (modals not opening) had many possible causes and gave no obvious clue about HTML comments.

We also found that decorative Unicode box-drawing characters (═ and ─) used inside /* */ JavaScript comments caused the same family of problem. Some browsers reject non-ASCII characters in script blocks entirely, even inside comments. Those had to be replaced with plain hyphens and equals signs.

The Fix

Replace any <script> or </script> tags written inside HTML comments near actual script blocks with plain descriptive text:

<!-- BOOTSTRAP JS BUNDLE - always last, just before </body>
     Without this JS bundle, the data-bs-* attributes won't work.
-->

The word “bundle” is clearer anyway.

The Rules to Remember

  1. Never write <script> or </script> inside an HTML comment that is near a real script block. The HTML5 parser will sometimes treat it as a real tag opener even inside a comment.
  2. Never use non-ASCII characters (Unicode box-drawing, smart quotes, em-dashes, etc.) inside a <script> block — even inside /* */ comments. Stick to plain ASCII. Those decorative characters are fine in HTML; they are not safe in JavaScript.
  3. When a JS error points to a line that doesn’t exist in your source file, the parser got confused long before that line. Work backwards from the last real script block and check what’s just outside it.
DWR
06/06/2026

TNT Image Gallery — TTG Acronym Tip + Precision Link to the Exact About Us Section

Read more…

We had a gallery comment that said TTG, but curious students had to hunt around manually to decode it. The navigation improvement was simple: make the acronym self-explanatory and link directly to the exact profile section in About Us.

Step 1: Clarify the acronym in-place

In the Slide Rule entry, TTG was wrapped with an abbreviation tooltip so hovering the acronym reveals its full meaning:

<abbr title='Tech Tools Guru'>TTG</abbr>

Step 2: Add a deep-link target to About Us

The TTG card in About Us received a stable anchor id:

<div class="about-card" id="ttgProfile">...</div>

Then the gallery link points directly there:

<a href='aboutUs.html#ttgProfile'>About Us</a>

Why This Is a Eureka

This is small-code, high-impact navigation design: students click once and land exactly where context lives. It reduces friction, reinforces acronym literacy, and models a best practice for internal documentation links across the site.

Live example: Open the Circular Slide Rule gallery entry and click its About Us link.

Eureka
06/06/2026

TNT Image Gallery — Neat Trick: Open a Specific Modal from a Hyperlink

Read more…

We wanted a link on Loud Pictures movie page that did more than navigate to the gallery. The goal was to jump to the gallery and automatically open the exact card modal for Loud Pictures. It worked with one query parameter and a tiny startup script.

Step 1: Put the target title in the link URL

The link passes the card title through ?open=...:

<a href="../tnt_image_gallery.html?open=Loud%20Pictures#galleryGrid">
  Open the gallery entry directly
</a>

Step 2: Read the URL and click the matching card

On gallery page load, JavaScript checks for open, finds the matching data-title, scrolls it into view, and triggers a click so Bootstrap opens the modal:

const openTitle = new URLSearchParams(window.location.search).get('open');
if (openTitle) {
  const cards = document.querySelectorAll('.gallery-item');
  const targetCard = Array.from(cards).find(card => card.dataset.title === openTitle);

  if (targetCard) {
    targetCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
    setTimeout(() => targetCard.click(), 250);
  }
}

Why This Is a Eureka

This creates deep-link behavior without duplicating pages. One reusable modal still powers the whole gallery, but outside pages can point directly to a specific entry by title. Students click one link and land exactly where the lesson needs them.

Live example: Open Loud Pictures movie page, then click “Open the gallery entry directly.”

Eureka
06/06/2026

TNT Image Gallery — Let CSS Stamp AI Entries with a Robot Marker

Read more…

Originally we were about to manually type a robot marker in front of every gallery blurb generated by Claude. Then we realized the browser can do that automatically with a CSS pseudo-element. That turned a repetitive editing job into one clean styling rule. You can see the live note in the gallery intro here: AI marker note in TNT Image Gallery.

How the Technology Works

A pseudo-element is a virtual element created by CSS. It is not written in the HTML, but it behaves like content that appears before or after an element’s text. With ::before, you target a class (in our case .aiEntry) and tell CSS to inject text using the content property.

<!-- Step 1: wrap AI-authored blurbs in a semantic class -->
<button data-desc="<span class='aiEntry'>AI-generated blurb text...</span>">
    ...
</button>

/* Step 2: style the AI blurb text itself */
.aiEntry {
    color: #6d6e6e;
    font-family: 'Courier New', Courier, monospace;
    display: block;
}

/* Step 3: inject marker content before every AI blurb */
.aiEntry::before {
    content: "🤖:";
    font-weight: bold;
    margin-right: 0.5em;
}

Translation: every element with class aiEntry gets a robot label automatically at render time. No copy/paste. No risk of forgetting one entry. Update the marker once in CSS and the whole gallery updates instantly.

Quick swap trick. If you ever want a different prefix, change just one line:

.aiEntry::before { content: "AI:"; }

That single edit updates every AI marker in the gallery.

Why This Was a Eureka

This is a textbook separation-of-concerns win. HTML holds meaning and content; CSS controls presentation. The blurbs stay clean, while the “AI speaker tag” lives in one reusable design rule. Less manual work, fewer mistakes, and a style system that scales.

Practical Caveat

Generated content from CSS is mainly visual. If a prefix is mission-critical information (not just a display cue), consider including that meaning in real HTML too. For our gallery context, the pseudo-element marker is exactly the right level of lightweight UI annotation.

Eureka
06/05/2026

Ask Copilot — “Your <article> has no heading” & the role="banner" Misuse

Read more…

When we ran the HTML validator on the Ask Copilot page, it came back with about ten identical warnings and one extra. All of them were quiet, easy-to-miss validator info messages — not hard errors, but still worth caring about. Here’s what they meant and why we fixed them.

Warning 1 (×10): “Article lacks heading”

What it means. In HTML, an <article> element is supposed to be a self-contained piece of content — like a newspaper article, a blog post, or (in our case) a Q&A entry. The HTML spec says each one should have a heading so that screen readers can announce what the article is about before reading it. Think of it like giving each newspaper column entry a headline: without one, a blind reader hears all the content but never hears a title.

Why we caused it. Each Ask Copilot entry had a label like “Entry #010 • Jun 2026” styled as a <span>. A <span> is invisible to screen readers as a structural landmark — it’s just a styling hook. The browser can’t tell it apart from any other text on the page.

The fix. We promoted each span to an <h3> element, which is a proper heading. Since browsers render <h3> with big text and margins by default, we had to add a small CSS reset to make it look exactly like the old span — same tiny uppercase text, no extra spacing.

/* Reset heading defaults so it looks like the original span */
.column-entry-num {
    font-size:   0.66rem;
    font-weight: 700;
    margin:      0;         /* ← the key reset — removes h3 margin */
    padding:     0;
    font-family: inherit;   /* don't use the heading font stack */
}

Why should you care? Roughly 1-in-25 people uses assistive technology at some point. Writing semantic HTML costs nothing extra — it’s just choosing the right tag. Using <h3> instead of <span> is the same amount of code; one means something to the browser, and one doesn’t. The visual result is identical. The accessible result is completely different.

Warning 2 (×1): role="banner" in the Wrong Place

What it means. ARIA roles tell screen readers what a section of the page is. The banner role specifically means “this is the site’s main header — the logo and navigation at the very top.” Every page is allowed exactly one banner, and it must be at the top level of the document, not buried inside <main>.

Why we caused it. The Ask Copilot page has a newspaper-style masthead inside the article column (the big “ASK COPILOT” nameplate). Someone added role="banner" to it because it looks like a banner. But that’s CSS’s job. ARIA roles describe structure, not appearance.

The fix. Remove the role entirely. The masthead is just a styled decorative element — no ARIA role needed. The page’s real site banner is the <nav> at the top, which Bootstrap handles automatically.

The Takeaway. Use CSS to control how things look. Use HTML structure and ARIA roles to describe what things are. Those are two completely separate jobs. Mixing them up breaks accessibility without breaking anything visible — which is exactly the kind of silent bug that’s hardest to catch.

DWR
06/03/2026

Hero Background Bleeds Outside the Image on Small Screens — background-size: cover to the Rescue

Read more…

First: how did we even find this? We tested the page at different screen sizes — shrinking the browser window down to phone width to see how it looked. That one habit caught this bug immediately. Always test at multiple screen sizes. A page that looks great on your laptop can be broken on the phone in your pocket, and you’ll never know unless you look.

The Problem

Imagine you hang a wide poster on a narrow wall. The poster is wide enough to cover the wall perfectly — but now imagine making the wall skinnier and skinnier. The poster shrinks with it, and eventually it’s so short that it doesn’t reach the bottom of the wall anymore. Bare wall shows below it.

That’s exactly what was happening to our hero section on phones. The CSS instruction background-size: 100% auto tells the browser: “make the image exactly as wide as the container, and figure out the height automatically (keep the proportions).” On a wide monitor that’s perfect. On a 390px-wide phone, the image width shrinks to 390px, and its proportional height drops too — often below the hero’s minimum height. So the bottom of the hero box had no photo behind it, just the browser’s default background color showing through, and the text was floating over nothing.

The Fix

The fix is a different CSS value: background-size: cover. Think of it like a different instruction to the browser: “scale the image until it completely fills the box — no bare spots allowed — even if that means cropping a little off the sides.” The whole hero box is always covered, no matter how narrow the screen gets.

We only needed this fix on small screens, so we used a media query — a CSS rule that only activates when the screen is below a certain width:

@media (max-width: 520px) {
    #hero {
        background-size: cover;
    }
}

Translation: “If the screen is 520 pixels wide or less, switch to cover mode.” Wider screens keep the original 100% auto look. Narrow screens switch to cover automatically. One problem, one fix, zero tradeoffs.

The Takeaway

background-size: 100% auto = “match my width exactly” (can leave gaps vertically on small screens).
background-size: cover = “fill the whole box, crop if needed” (no gaps, ever).
Media queries let you use different rules for different screen sizes — one of the most powerful tools in responsive design.

DWR
06/03/2026

rel="noopener noreferrer" — Why It Matters on Every External Link

Read more…

When you open a link in a new tab with target="_blank", the new page secretly gets a handle back to your page via window.opener. A malicious site could exploit that to silently redirect your original tab — a classic attack called reverse tabnapping:

// evil code on the destination site could do:
window.opener.location = "https://fake-login-page.com";

rel="noopener" cuts that connection: the new tab opens with window.opener === null, so the linked site can’t touch your page at all.

rel="noreferrer" goes one step further — it also suppresses the Referer HTTP header, so the destination site doesn’t even know which page sent the visitor. Best practice: use both together.

<a href="https://example.com" target="_blank"
   rel="noopener noreferrer">Link text</a>

It’s a free, zero-cost security habit. Add it to every target="_blank" link — always.

Eureka
06/03/2026

Fresh Start — 2026 TNT Upgrade

Read more…

As part of the 2026 TNT redesign, the old DWR & Eureka log was retired and this fresh version was born. The previous log (2022–2023) covered Bootstrap 5.0 quirks, case-sensitive filenames on servers, and data-type disasters in JavaScript — all still great lessons, preserved in origDwrEureka.html for posterity.

New entries will document discoveries and disasters from the 2026 upgrade onward: Bootstrap 5.3, CSS custom properties (design tokens), Font Awesome 6.5, separation-of-concerns CSS architecture, and whatever we stumble into next.

Eureka