Mama’s Little Function — S.P.A.R.K. Chat Log
A 2022 Bootstrap 5.0 math-song app gets the full 2026 TNT treatment — standard navbar, hero section, SPARK bar, external stylesheet, and preserved song logic.
This log documents the upgrade of a 2022 JavaScript app — a song about mathematical functions — from its original Bootstrap 5.0.2 standalone shell into the TNT 2026 ecosystem. The song content, verse-reveal logic, and noscript teaching demo were preserved exactly. Everything changed around them.
Watch for S.P.A.R.K. banners framing each phase, decision boxes explaining each shell change, and a before/after summary showing what the original had versus what the upgraded version has. The core lesson: a well-designed app can survive a complete shell renovation if the core logic is kept intact.
I want to upgrade mamasFunctionHome.html to fit into our current TNT ecosystem. It is a JavaScript themed app like the attached dragDropIndex.html. For consistency, I’d like to use the same type of navbar and mini-navbar structure in the mamasFunction app as is used in the dragDropIndex file. I like the white background color of the app itself, and I see no need to change any of the ‘workings’ of the app at this time. Because this is a SPARK application, I’d like to create mamasFunctionChatlog.html and link to it from the app’s page as we did in the dragDrop app with its chatlog. Once we get this all working and looking correct, there are a couple of upgrades I’d like to implement. The blender icon is in the root folder’s index subfolder.
The phrase “I see no need to change any of the ‘workings’ of the app” is doing important work here. It draws a precise boundary between shell (navbar, hero, footer, CSS organisation) and core (song content, verse-reveal logic, noscript demo, JavaScript functions). Without that boundary, an upgrade might inadvertently improve the JavaScript away from the original pattern, breaking the noscript demo or altering the intentional progressive-reveal behaviour.
The reference to dragDropIndex.html as a layout model is also highly efficient. Rather than re-specifying the navbar structure, the SPARK bar format, the footer pattern, and the Bootstrap version from scratch, the prompt points at an already-correct implementation. This is exactly how the TNT ecosystem is supposed to work: each new app references an existing one as a structural template rather than re-inventing the structure from a description.
The phrase “there are a couple of upgrades I’d like to implement” (after the main work) is good practice: it separates the foundational ecosystem integration from the content improvements. Getting the shell right first means the improvements land in a clean, standards-compliant context rather than on top of half-migrated scaffolding.
- Bootstrap 5.0.2 via CDN — already Bootstrap 5, so no
data-toggle→data-bs-togglemigration needed - App-specific navbar: blender icon + brand name + three links (text file, TNT, TNT JS Apps). Not the standard TNT sticky navbar.
#jumbotrondiv: Bootstrap primary blue background witheasyBakeOvenDark.jpgbackground image, Dancing Script font heading. Served as the page hero.- Inline
<style>block: all page-specific CSS inline in the<head>. No external page-specific stylesheet. - Old footer pattern: light Bootstrap footer with nav icons, inline copyright span, an extra tnt-sandbox.com link, and the validate link pointing to
../../images/(broken path). - Moroccan flower background:
main, div#primaryContenthad a subtle decorative background image applied. - Google Fonts: Dancing Script (h1 in lyrics), Dosis (lyrics text body). No Boogaloo or Inter.
- No Font Awesome, no SEO meta tags, no OG tags, no TNT base styles.
- The song and JavaScript: complete and correct. Verse-reveal logic, setup/init pattern, noscript demo — all working. Zero need to touch them.
The original used Bootstrap 5.0.2 released in 2021. Bootstrap 5.3.3 is the current stable release. Unlike the Bootstrap 4 → 5 upgrade on the Drag & Drop Circles app (which required renaming every data-toggle attribute and removing jQuery), this upgrade was purely cosmetic: update the CDN links and SRI hashes. No API changes affected this app between 5.0.2 and 5.3.3 — the only data-bs-* attribute in the song is on the navbar toggler, which was already correctly formed.
<link href="bootstrap@5.0.2/..."
integrity="sha384-EVSTQN3...">
...
<script src="bootstrap@5.0.2/..."
integrity="sha384-MrcW6...">
<link href="bootstrap@5.3.3/..."
integrity="sha384-QWTKZy...">
...
<script src="bootstrap@5.3.3/..."
integrity="sha384-YvpcrY...">
The original navbar was an app island: it pointed back to the app’s own root, listed three app-specific links, and had no relationship to the TNT site structure. The replacement is the standard TNT sticky dark navbar (identical to every other 2026 TNT app), with JS Apps marked active — the same classification as the Drag & Drop Circles app, since Mama’s Little Function is powered entirely by JavaScript.
The SPARK bar sits below the navbar and carries a single link: this chatlog. It also carries the classification label “JS App” and the date. This is the standard mini-navbar pattern established in the TNT 2026 ecosystem.
The original #jumbotron used Bootstrap’s primary blue background with the easyBakeOvenDark.jpg background image applied via CSS, a display-4 heading in Dancing Script, and no TNT structure. The replacement hero section uses the same easyBakeOvenDark.jpg — the image was already in the app’s local images/ folder — with a dark overlay gradient, a Font Awesome blender icon (amber, matching TNT accent color), and the Boogaloo hero title font.
The blender icon in the hero is fas fa-blender (Font Awesome 6.5 built-in). The original app used the custom blender SVG from the root images/ folder only in the old navbar. The Font Awesome version serves the hero cleanly without any additional asset dependency. The user noted the custom SVG exists in the root images folder — it remains available for use in future upgrades.
<style> → External StylesheetAll page-specific CSS was moved to styles/mamasFunctionStyles.css. The old stylesheet also had a separate styles/mamasFunction-styles.css with jumbotron background rules, mastheadIcon sizing, an old footer pattern, and media-query breakpoint-indicator borders (colored nav borders at each Bootstrap breakpoint — a teaching demonstration of responsive design). In the new version:
- The jumbotron and mastheadIcon rules were removed (those elements no longer exist).
- The breakpoint-indicator nav borders were removed (the TNT navbar does not use this pattern).
- The old footer CSS was removed (replaced by the shared TNT footer from
tnt-base-styles.css). - The lyrics-specific rules (
#lyrics,#chorusDiv,p.left,p.right,h4clear) were preserved exactly and moved into the new external file, with theh4rule scoped to#lyrics h4to avoid affecting headings elsewhere. - The
strongrule (uppercase, italic, brown) was scoped to#appSection strongfor the same reason. - The moroccan-flower background was dropped; a white
#appSectionbackground replaces it per the user’s preference.
The custom color classes (.text-custom-color1, .bg-custom-color1) and the font aliases (.googleFont1 for Dancing Script, .googleFont2 for Dosis) were preserved unchanged in the new external stylesheet, since the HTML still uses them.
The original init() function handled copyright/update spans, the validate link, and called setup(). It referenced a validateLi element that existed in the old footer but does not exist in the TNT standard footer. Calling getElementById("validateLi") in the new version would return null, causing a TypeError when classList.add("d-none") was called on it.
The fix: init() now handles only the copyright/update spans and calls setup(). The validate link is handled by the standard TNT IIFE pattern at the bottom of the body (same as all other 2026 TNT apps), which checks for the validateLink element and gracefully does nothing if it is absent. The song logic — setup(), showNextVerse(), all array operations — was preserved byte-for-byte including the //dwr! comment.
The original noscript block hides div#primaryContent, footer when JavaScript is disabled. In the new structure, the primary content wrapper is section#appSection, not div#primaryContent. The noscript’s inline style was updated to target div#appSection, footer.
Everything else in the noscript block is unchanged: the “enable JavaScript” message, the Keep Calm image, the link to the legacy noscript demo, and the intentionally non-validating <style>-inside-<noscript> pattern (which has a comment explaining the compromise). The link to the legacy app was updated to the correct path from the new folder location: ../_LegacyTNT/MamasFunctionNoScript-JSBS52022-10-21/index.html.
| File | Status | What changed |
|---|---|---|
mamasFunctionHome.html |
Updated |
Bootstrap 5.0.2 → 5.3.3 CDN links. App-specific navbar removed → TNT standard navbar (JS Apps active).#jumbotron removed → TNT hero section (easyBakeOvenDark.jpg background, Font Awesome blender icon).SPARK bar added (links to this chatlog). Old footer replaced → TNT standard footer. Inline <style> block removed → styles/mamasFunctionStyles.css. Font Awesome + Boogaloo + Inter added.SEO + OG meta tags added. init() simplified; validateLink moved to IIFE.Song content, verse logic, noscript block, setup/showNextVerse: unchanged. |
styles/mamasFunctionStyles.css |
New | SPARK bar, hero (easyBakeOvenDark.jpg), app section (white), lyrics/chorus layout, custom color + font classes. Stage 2: display: flow-root added to #lyrics; clear: both extended to #lyrics h3 as well as h4. |
mamasFunctionChatlog.html |
New | This document. |
js_apps.html |
Updated | Figure added for Mama’s Little Function using the root-images blender icon, placed before the “More App Goodness” placeholder. |
news.html |
Updated | New accordion entry #049 added at the top of the 2026 section. |
When upgrading within Bootstrap 5 (e.g., 5.0 → 5.3), the work is minimal:
- Update CDN links — new SRI hash for both CSS and JS.
- No data attribute changes —
data-bs-*attributes are the same in all Bootstrap 5 versions. - No jQuery concerns — jQuery was already removed when Bootstrap 5 was first adopted.
- Scan release notes — check for deprecated utility class renames in the specific version range (e.g., some color utility renames in 5.3). For this app, no renames applied.
This is distinct from a Bootstrap 4 → 5 upgrade, which requires attribute migration, jQuery removal, and a full utility class audit. Knowing which kind of upgrade you are doing determines the scope of work before you open the first file.
Every TNT 2026 app uses this IIFE (Immediately Invoked Function Expression) at the bottom of the <body> to handle the footer’s validate link:
(function () {
var vLink = document.getElementById("validateLink");
if (vLink) {
if (window.location.href.includes("localhost")) {
vLink.style.display = "none"; // hide on local dev
} else {
vLink.href += "?doc=" + encodeURIComponent(window.location.href);
}
}
}());
The if (vLink) guard makes it safe even if the element is absent. The older pattern used in this app’s original init() function skipped the guard: validateLi.classList.add("d-none") would throw a TypeError on null. The fix: move the validate link logic out of init() entirely and into this IIFE, which is the standard pattern for all 2026 TNT apps.
Stage 2 Float Containment Fix
The ‘JavaScript’ part of this app is that when the novice hits the ‘Next Verse’ button, it appears. The problem is that on the first click, when a new verse appears, it runs over the footer area (it’s hard to read and looks tacky). We need to make sure that when the button is pushed and new info appears, the page makes room for the new info, pushing the footer down, appropriately. Can you fix this and discuss the way you did it in the chatlog?
Two lines of CSS fixed it. Here’s why it was happening and what changed.
The revealed stanzas use p.left { float: left; width: 50%; } and p.right { float: right; width: 40%; }. Floated elements are removed from the normal document flow. This means their parent container — #lyrics — does not count them when calculating its own height. As long as the floated paragraphs are taller than the non-floated content still inside #lyrics, the container appears to end before the floats do, and the floats overlap whatever comes next in the page — in this case, the footer.
This is one of the oldest, most-documented CSS layout gotchas. It has a name: float overflow or float containment failure. It does not mean the page is broken; it means the container was never told it needed to grow around its floated children. The fix is to create a Block Formatting Context (BFC) on the container. A BFC always contains its floated descendants: the container expands to wrap them, and the footer stays where it belongs.
display: flow-root on #lyricsAdding display: flow-root to the div#lyrics rule creates a Block Formatting Context. The flow-root value was added to CSS specifically for this purpose: it generates a BFC just like a block container, with no side effects. Every floated child of #lyrics now stays inside the box.
div#lyrics {
font-size: 1.3em;
max-width: 700px;
border-radius: 50px;
margin: 0 auto;
/* no BFC — floats escape */
}
div#lyrics {
font-size: 1.3em;
max-width: 700px;
border-radius: 50px;
margin: 0 auto;
display: flow-root; /* BFC: floats stay inside */
}
Why display: flow-root rather than overflow: hidden? Both create a BFC. But overflow: hidden also clips content that visually extends beyond the element’s box edge — which can affect drop shadows, tooltips, or absolutely positioned children. display: flow-root creates a BFC only; it clips nothing. It is the modern, purpose-built solution.
Why not clearfix? The traditional clearfix (::after { content: ""; display: table; clear: both; }) works by inserting an invisible block after the floated content to force the container to stretch. display: flow-root is the current standard recommendation and achieves the same result with a single property declaration.
#lyrics h3 Added to the clear: both RuleInside #lyrics, after each stanza pair (p.left + p.right), a chorus heading appears. The existing CSS rule #lyrics h4 { clear: both; } ensured that the second and third chorus headings (id="c2" and id="c3", both <h4>) started below their preceding floats. However, the first chorus heading (id="c1") was changed to an <h3> element, so it was not covered by the #lyrics h4 rule. Without clear: both, c1 would try to start alongside the floated stanzas rather than below them, and the second stanza pair would stack incorrectly.
/* only h4 cleared — h3#c1 was missed */
#lyrics h4 {
clear: both;
text-align: center;
margin-bottom: 0;
}
/* h3 = c1, h4 = c2/c3 — all chorus headings clear */
#lyrics h3,
#lyrics h4 {
clear: both;
text-align: center;
margin-bottom: 0;
}
Together, Fix 1 (BFC on #lyrics) and Fix 2 (clear on both h3 and h4) ensure that: (1) the container always grows to contain all floated stanza content, and (2) each new stanza pair starts below the previous one rather than alongside it. The footer is pushed down correctly by the natural block flow of the now-contained lyrics section.
The JavaScript was doing exactly what it was supposed to: removing the hidden attribute from the paragraph elements. The problem was never in the script — the paragraphs were being revealed correctly. The problem was that CSS had no instruction about what to do with floated elements inside a container that doesn’t establish a Block Formatting Context.
This is an important distinction. When a page behaves unexpectedly after a DOM change, the issue can be in one of three places: the JavaScript that made the change, the HTML structure the change operates on, or the CSS rules that govern how the changed elements are laid out. In this case the script was correct, the structure was correct, and the CSS was simply missing two properties. Developing the ability to diagnose which layer contains the bug — without assuming it must be the JavaScript just because JavaScript triggered the event — is one of the most useful skills a web developer can acquire.
Stage 3 Easter Egg — Function Evaluator
Sometimes I like to hide little ‘Easter eggs’ in an app to see if students can find them. In this case, I really like the blender icon in the hero. Let’s make it ‘clickable’ (with a screen tip: ‘Click me!’) and when clicked, let’s launch a modal that sports a form that lets a user specify a slope and a y-intercept (default values of 3 and 2 respectively) and a place to enter a ‘x’ (default of 2). These values match the first verse of the song we are showcasing. On hitting a button, ‘Run’ the output should be given and presented like this: x → f(x) → b + m*x → y → (x, y) which shows how a ‘x’ is fed to the function f which is like plugging it into the expression b + m*x which is a ‘y’ and the two form an ordered pair as is suggested in the song. There should be buttons to clear the form. Let’s include this development in the chatlog. Let’s call the function ‘f’ to match the song as well.
Three changes to two files. Here’s the full design breakdown.
An Easter egg has to be discoverable without instructions. The blender icon was already in the hero because the metaphor (“a function is like a blender”) is central to the app. Making it clickable adds a second layer: students who pay close attention to the page and hover over things will find it; students who don’t will never know it’s there. The screen tip “Click me!” is the reward for curiosity — it appears on hover as a native browser tooltip from the button’s title attribute.
The hover animation (scale + slight rotation + amber glow) provides a secondary discovery cue: students who see the icon move when their cursor passes over it will investigate. Both signals — tooltip and motion — point to the same action.
The blender <i> element was wrapped in a <button> with Bootstrap’s data-bs-toggle="modal" and data-bs-target="#functionModal". The button carries zero visual chrome (btn p-0 border-0 bg-transparent) so it renders identically to the plain icon; a custom class blender-egg applies the hover/focus animation via CSS. The aria-label ensures screen-reader users know it opens a function evaluator.
<i class="fas fa-blender me-2" style="color: var(--tnt-amber); ..." aria-hidden="true"></i>
<button type="button"
class="btn p-0 border-0 bg-transparent blender-egg"
data-bs-toggle="modal"
data-bs-target="#functionModal"
title="Click me!"
aria-label="Open function evaluator">
<i class="fas fa-blender me-2"
style="color: var(--tnt-amber); ..."
aria-hidden="true"></i>
</button>
The title="Click me!" attribute delivers the tooltip via the browser’s native implementation: no Bootstrap tooltip JavaScript initialization required, works across all browsers, and can never be blocked by a Content Security Policy. Bootstrap’s tooltip component was considered but rejected for this reason — a native title attribute is simpler, more reliable, and sufficient for a single element.
The modal header (“Feed the Function!”) echoes the song: Mama’s little function eats xs. The three inputs are a Bootstrap 3-column row, each labelled in purple. Default values are deliberately set to match the first verse — m = 3, b = 2, x = 2 — so the first thing a student sees after clicking Run is exactly the arithmetic the song describes: f(2) = 2 + 3·2 = 8. The default state is itself a teaching moment.
The footer has two buttons: Clear (resets all three inputs to their defaults and hides the output) and Run (executes the calculation). The clear button uses Bootstrap’s outline-secondary style; the run button matches the modal header’s purple for visual coherence. Neither closes the modal, so students can experiment with multiple values in a single session without re-opening it.
x → f(x) → b + m·x → y → (x, y)The output chain shows every step of what a function actually does, in the order the song describes them. Each step is a distinct color: the input x is purple (the app’s dominant color), the function application f(x) is purple italic, the expanded expression is green, the output y is red, and the ordered pair is purple bold. The arrows between steps are gray so they recede visually and the values stand forward.
/* Example with defaults: m = 3, b = 2, x = 2 */ 2 → f(2) → 2 + 3·2 → 8 → (2, 8)
The fmtNum() helper handles number display: integers show without a decimal point; non-integers are capped at four decimal places and trailing zeros are trimmed. This means f(1/3) displays as 0.3333 rather than 0.33333333. Negative slope values are wrapped in parentheses ((−3)·2 rather than −3·2) to prevent the display from reading ambiguously as subtraction.
The output <div> starts hidden (d-none), and runFunction() removes that class on first run. A validation guard checks all three fields for NaN and shows a red error message rather than displaying NaN in the chain — important because parseFloat("") returns NaN and students who clear a field would otherwise see meaningless output.
| File | What changed |
|---|---|
mamasFunctionHome.html |
Blender <i> wrapped in <button class="blender-egg"> with modal trigger and title="Click me!".Bootstrap modal #functionModal added (three inputs, output chain div, Run + Clear buttons).New <script> block: fmtNum(), runFunction(), clearFunction().
|
styles/mamasFunctionStyles.css |
.blender-egg — hover/focus scale + rotation + amber glow..function-output-chain — purple-bordered flex row for the output chain..chain-x, .chain-fx, .chain-expr, .chain-y, .chain-pair, .chain-arrow — per-step color coding.
|
Stage 4 Icon Size Regression Fix
The little icon got substantially smaller with the upgrade; otherwise, I’m very happy with it; can you get it back to be the size it was originally? Why did you need to shrink it; there may have been a reason. Let’s elaborate on this in the chatlog.
It wasn’t a deliberate choice — it was an accidental side effect of adding the Bootstrap button wrapper. One CSS property fixes it. Here’s the full explanation.
The blender icon’s size is set with font-size: 0.8em in its inline style. The em unit is relative to the computed font-size of the element’s parent. Before Stage 3, the parent was the <h1 class="hero-title">, which the CSS sets to clamp(2.2rem, 5.5vw, 3.8rem). The icon resolved to roughly 0.8 × 3.8rem = 3rem at desktop width — large and prominent in the hero.
In Stage 3, the icon was wrapped in a <button class="btn ...">. Bootstrap’s .btn rule sets font-size: 1rem explicitly. This breaks the inheritance chain: instead of the button inheriting the h1’s large font-size and passing it to the icon, the button hard-codes 1rem. The icon then resolved to 0.8 × 1rem = 0.8rem — a dramatic reduction with no intentional design behind it.
This is a common Bootstrap gotcha. Many Bootstrap component classes (buttons, inputs, badges, form controls) explicitly set font-size for cross-context consistency. When you nest content inside them that relies on font-size inheritance, that inheritance is severed at the component boundary. The original icon relied on it; the wrapper destroyed it.
font-size: inherit on .blender-eggAdding font-size: inherit to the .blender-egg CSS rule restores the inheritance chain. inherit forces a CSS property to take the computed value of its parent instead of any default or browser/framework value. The button now inherits the h1’s large font-size, and the icon’s 0.8em is once again relative to the h1.
.blender-egg {
cursor: pointer;
transition: transform 0.2s, filter 0.2s;
line-height: 1;
/* Bootstrap .btn set font-size: 1rem here */
/* icon 0.8em → 0.8 × 1rem = 0.8rem */
}
.blender-egg {
cursor: pointer;
transition: transform 0.2s, filter 0.2s;
line-height: 1;
font-size: inherit; /* pass h1 size through */
/* icon 0.8em → 0.8 × h1-size ≈ 3rem */
}
Why not just remove font-size: 0.8em from the icon and use the h1 size directly? The 20% reduction was intentional in the original design — the icon is slightly smaller than the title text so it reads as an accent rather than competing for visual weight. Keeping the 0.8em on the icon and using inherit on the button preserves both the relative sizing and the inheritance fix.
Why wasn’t this caught in Stage 3? The button was added and the icon appeared in the same hero, at the same position, with the same amber color. The size difference is obvious on a large screen side-by-side but easy to miss when only looking at the result of a single round of changes. This is exactly the kind of regression that a structured review — or a user who knows what “before” looked like — catches.
Bootstrap component classes are designed to look correct in isolation. To achieve that, many of them explicitly set properties — font-size, line-height, color, text-decoration — rather than inheriting them from the surrounding context. This is a feature, not a bug: it means a button always looks like a button regardless of whether it’s inside a paragraph, a heading, or a card. But it becomes a problem when you deliberately want the component to inherit its context.
The solution is always the same: identify which property is being reset by the component class, and use inherit to explicitly restore the inheritance. The properties most commonly reset by Bootstrap components are:
font-size— on buttons, badges, form controls, inputscolor— on links, buttons, alerts (where Bootstrap sets a specific palette color)line-height— on buttons and form controlstext-decoration— on links styled as buttons (.btn)
Stage 5 Next Verse Button Hover Fix
When I hover over the ‘Next Verse’ button, it fades and is difficult to read; where is this happening and let’s make it an outline button with the text becoming the color of the button when it’s not being hovered over. Please include this discussion in the chatlog.
The fade had a specific cause in how Bootstrap’s hover cascade interacts with a background utility class. One CSS class swap and a new rule fixed it cleanly.
!important Cascade TrapThe original button used two Bootstrap utility classes: bg-custom-color1 (the custom purple background) and text-light (white text). The problem was in bg-custom-color1:
/* In mamasFunctionStyles.css */
.bg-custom-color1 { background-color: var(--mlf-purple); }
/* No !important — survives in normal flow but loses to higher-specificity hover rules */
Bootstrap’s base .btn:hover rule sets background-color: var(--bs-btn-hover-bg). When no specific button variant (like btn-primary) sets --bs-btn-hover-bg, this variable defaults to transparent. Because .bg-custom-color1 has no !important, the hover rule wins: the purple background evaporates on hover, replaced by the white body background. White text (text-light) on white body = invisible.
Bootstrap’s own .bg-* utility classes do use !important precisely to survive this hover cascade. Custom background utility classes that don’t include it silently lose to any Bootstrap component hover rule at the same specificity level.
.btn-outline-mlf via Bootstrap 5 CSS VariablesBootstrap 5 exposes all button states as CSS custom properties on the .btn element. Setting them on a custom class creates a fully-behaved outline button variant with no specificity fights:
<!-- HTML -->
class="btn bg-custom-color1 text-light"
/* CSS — bg-custom-color1 loses to btn:hover */
.bg-custom-color1 {
background-color: var(--mlf-purple);
/* no !important = overridden on hover */
}
<!-- HTML -->
class="btn btn-outline-mlf"
/* CSS — BS5 variables own every state */
.btn-outline-mlf {
--bs-btn-color: var(--mlf-purple);
--bs-btn-border-color: var(--mlf-purple);
--bs-btn-hover-color: #fff;
--bs-btn-hover-bg: var(--mlf-purple);
--bs-btn-hover-border-color: var(--mlf-purple);
--bs-btn-active-color: #fff;
--bs-btn-active-bg: var(--mlf-purple);
...
}
The result: normal state shows a purple border with purple text and no fill — the outline style the user requested. On hover, the fill appears (purple background, white text), which is the standard Bootstrap outline-button inversion. Bootstrap reads all the hover/active states directly from the CSS variables, so there is no cascade conflict.
This is also the correct pattern for any future custom button variants in the TNT ecosystem: define a .btn-outline-* or .btn-* class using the Bootstrap CSS variable set rather than stacking background/text utilities on top of .btn.
Bootstrap has two ways to style a component: (1) stack utility classes on top of it, or (2) set the component’s CSS variables. Approach (1) is quick but fragile — utility classes can be overridden by component pseudo-class rules at the same specificity level unless they carry !important. Approach (2) is robust — CSS variables cascade through the component’s own rules, so hover, focus, and active states all resolve correctly without specificity conflicts.
The rule of thumb: use utility classes to adjust spacing, layout, and typography. Use CSS variable overrides to define interactive states (hover, focus, active) on Bootstrap components. Mixing the two — utility classes for colour + Bootstrap component for interactivity — creates exactly the cascade trap that caused this fade.