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