Dấu ấn
Phong vị Núi cao

Qua từng
TÁCH TRÀ
COMING SOON
/* ===========================================================================
TT_Header_Tint_CSS — v2.2.0
Tư-Trà · adaptive header tint + WCAG-aware foreground
Author: Tư Trà (with Claude)
Paste into the Bricks Code element's CSS box (header template).
Companion: TT_Header_Tint_JS v2.6.0
WHAT THIS FILE DOES (and deliberately does NOT do)
- It does NOT draw the backdrop gradient. Your Header Appearance CSS owns
the backdrop and its gradient, which reads
rgba(var(--color-the-rgb), <stop alpha>). This file leaves that alone.
- The JS drives the gradient COLOR by overriding --color-the-rgb (tweened
frame-by-frame for a smooth fade). When no signal section is under the
sentinel line, the JS removes that override, so your Appearance gradient
falls back to its own default — untouched.
- For image sections (data-tt-header-transparent) the JS sets
--tt-header-bg-opacity to 0; .tt-header-bg-adaptive consumes it and
fades out, revealing the image through the header.
- FOREGROUND: the JS injects a forced color rule onto .tt-header-fg-adaptive
(and its a/button/svg descendants) with !important, because a plain class
loses to Bricks's own nav color rules. This file therefore does NOT set
color/fill/stroke on that class — it only provides a smooth transition.
The logo is driven separately by the header coordinator (not this class).
SETUP
- Tag the backdrop element with class "tt-header-bg-adaptive".
- Tag nav text containers and the hamburger toggle with
"tt-header-fg-adaptive". Do NOT tag the logo (the coordinator owns it).
- Tag tinting sections with "tt-header-signal"; image sections also get
data-tt-header-transparent + a representative background-color.
EDIT THESE
- --tt-header-fg-light / --tt-header-fg-dark : the two foreground
candidates (used by the JS for contrast). Set to your brand colors.
CHANGELOG
v2.2.0 — .tt-header-fg-adaptive no longer sets color/fill/stroke (the JS
now forces those past Bricks via an injected rule). This class
keeps only a color/fill transition for smoothness. Removed the
stroke declaration that was mis-coloring the logo outline.
v2.1.0 — Foreground class renamed; added .tt-header-bg-adaptive for the
transparent state; no id selectors.
v2.0.0 — Companion CSS no longer draws the gradient.
=========================================================================== */
:root{
/* Adaptive foreground candidates (read by the JS to choose the higher-
contrast option against each section's background-color). */
--tt-header-fg-light: #e9d8c8; /* cream */
--tt-header-fg-dark: #62704d; /* near-black */
}
/* The backdrop element. The JS sets --tt-header-bg-opacity to 0 for image
sections; this element consumes it and fades out. Defaults to 1 (visible)
when the var is unset, so your Appearance gradient shows normally. */
.tt-header-bg-adaptive{
opacity: var(--tt-header-bg-opacity, 1);
transition: opacity 600ms cubic-bezier(.4, 0, .2, 1);
}
/* Foreground transition only. The actual color is forced by the JS via an
injected !important rule (a bare class can't beat Bricks's nav color).
This rule just makes the change ease over 600ms. It does NOT set stroke —
the logo is colored by the coordinator as fill:currentColor, not here. */
.tt-header-fg-adaptive,
.tt-header-fg-adaptive a,
.tt-header-fg-adaptive button,
.tt-header-fg-adaptive svg{
transition: color 600ms cubic-bezier(.4, 0, .2, 1),
fill 600ms cubic-bezier(.4, 0, .2, 1);
}/* ===========================================================================
TT_Header_Tint_JS — v2.7.0
Tư-Trà · adaptive header tint + WCAG-aware foreground
Author: Tư Trà (with Claude)
Paste into the Bricks Code element's JavaScript box (Bricks auto-wraps
it in <script> tags). Companion: TT_Header_Tint_CSS v2.1.0
MODEL
ONE sentinel line lives inside the header. Each scroll frame we ask:
which signal sections (.tt-header-signal) currently SPAN that line?
Of those, the topmost in z-order wins. The script then:
- overrides --color-the-rgb with that section's color as an "R, G, B"
triple, so your Header Appearance gradient re-renders in that color;
- sets --tt-header-fg to the higher-WCAG-contrast foreground candidate
(measured against the section's solid background-color);
- for a section flagged data-tt-header-transparent, sets
--tt-header-bg-opacity to 0; the element you tag with
.tt-header-bg-adaptive consumes it and fades out, so an image
background shows through.
When NO signal section is under the line, the script REMOVES all of its
overrides, so the header falls back to your Appearance CSS untouched.
SETUP
1. Header wrapper gets the class/selector in HEADER_SELECTOR.
2. The backdrop element gets class "tt-header-bg-adaptive" (it fades
out for transparent sections — no id is hardcoded in the JS).
3. Each tinting section gets class "tt-header-signal".
4. Image sections also get data-tt-header-transparent AND a
background-color matching the image's dominant tone (foreground
contrast is measured against background-color, not image pixels).
5. Header text/icons/SVG that must stay legible get
"tt-header-fg-adaptive".
CHANGELOG
v2.7.0 — Foreground color now fades smoothly: it is interpolated in JS
(rAF) and written into the forced rule each frame, because a
CSS transition does not fire reliably on an !important value that
arrives via a stylesheet swap. Default sentinel position set to
"middle".
v2.6.0 — Foreground now FORCED past Bricks via an injected <style> rule
targeting .tt-header-fg-adaptive and its a/svg descendants with
!important (a bare class lost on specificity — that's why nav +
hamburger never changed). After forcing the nav color the script
calls coordinator.syncNavColor() so the logo follows as `color`
(the logo uses fill:currentColor; no stroke is applied). Bricks's
own color remains the fallback when the rule is emptied (no
signal). The CSS companion's .tt-header-fg-adaptive no longer
sets stroke/fill — JS owns the forcing now.
v2.5.0 — (1) Smooth background color fade: the RGB triple is now
interpolated frame-by-frame over FADE_MS (rAF), since a plain
triple can't be CSS-transitioned. Your gradient CSS is untouched.
(2) Foreground now handed to the coordinator (window.TT_HEADER)
so nav text, logo, and hamburger all follow — instead of relying
on a CSS class the more-specific Bricks rules were overriding.
The script probes for a coordinator setter and falls back to
setting --tt-header-fg; the HUD reports which path was used.
v2.4.0 — Fixed silent no-op: HEADER_SELECTOR default is now "#brx-header"
(Bricks's header id); the script waits for DOMReady before
running; and the debug HUD is built BEFORE the header lookup so
a missing/mismatched header shows "header: NOT FOUND" instead of
dying silently. Matches the proven TT_Header_Hold HUD pattern.
v2.3.1 — Debug parameter renamed ttdebug=1 → tthtint=1 to avoid colliding
with another module already using ttdebug. DEBUG_PARAM const
added so the trigger word is easy to change.
v2.3.0 — Debug now activates via a URL parameter — no code edit needed to
toggle, and zero overhead in production when absent. The
FORCE_DEBUG const still works as a manual force-on override.
v2.2.0 — Added on-screen debug HUD (DEBUG=true): shows signal-section
count, active section, the color read, the resolved RGB triple,
transparent flag, and sentinel position. Helps diagnose setup.
v2.1.0 — Background transparency now driven via --tt-header-bg-opacity
(consumed by .tt-header-bg-adaptive) instead of selecting the
backdrop element by id — JS no longer hardcodes a backdrop
selector. Foreground class renamed .tt-header-adaptive →
.tt-header-fg-adaptive (CSS-side; JS unaffected by that rename).
v2.0.0 — Reworked: drives the existing Appearance gradient by overriding
--color-the-rgb (R,G,B triple) instead of drawing its own
gradient; transparency via backdrop opacity; no-signal clears
all overrides so Appearance CSS shows through. No @property /
relative-color dependency (older-Safari friendly). Color switch
is instant; opacity + foreground still animate via CSS.
=========================================================================== */
(function(){
"use strict";
function boot(){
/* ─── CONFIG — EDIT THESE ──────────────────────────────────────────────── */
/* Header wrapper selector (where the sentinel line is injected). */
var HEADER_SELECTOR = "#brx-header";
/* The CSS variable your Appearance gradient reads for its color
(an "R, G, B" triple). The script overrides this per section. */
var RGB_VAR = "--color-the-rgb";
/* Class on page sections whose background should tint the header. */
var SIGNAL_CLASS = "tt-header-signal";
/* Attribute that makes a section's backdrop go fully transparent. */
var TRANSPARENT_ATTR = "data-tt-header-transparent";
/* Where the sentinel line sits, measured DOWN FROM THE HEADER TOP:
"top" → header top edge
"middle" → header vertical center
"bottom" → header bottom edge (the natural choice)
"<n>%" → percentage of header height, e.g. "75%"
"<n>px" → fixed pixels from header top, e.g. "60px" */
var SENTINEL_POS = "middle";
/* Two foreground candidates for .tt-header-fg-adaptive elements. Each may
be any CSS color OR a CSS variable. Higher WCAG contrast wins. */
var FG_LIGHT = "var(--tt-header-fg-light)";
var FG_DARK = "var(--tt-header-fg-dark)";
/* Background color fade duration (ms). The gradient color is a plain RGB
triple that CSS can't transition, so the script tweens it via rAF. */
var FADE_MS = 600;
/* The class you put on elements that must take the adaptive foreground
(nav UL / links, hamburger toggle). The script forces color onto this
class and its a/svg descendants, beating Bricks's own color rules. */
var FG_CLASS = "tt-header-fg-adaptive";
/* The header coordinator (window[COORDINATOR]). After the script forces
the nav color, it calls coordinator.syncNavColor() so the logo — which
the coordinator owns — re-reads and follows. The logo is NOT tagged
with FG_CLASS; it is driven only through the coordinator. */
var COORDINATOR = "TT_HEADER";
/* Debug activation. Add ?tthtint=1 (or &tthtint=1) to the page URL to
turn on the magenta sentinel bar, the on-screen HUD, and the WCAG-AA
console warning. Named uniquely so it does not clash with other TT
modules' debug params. Off otherwise — zero overhead in production.
Set FORCE_DEBUG true to force it on regardless of the URL. */
var DEBUG_PARAM = "tthtint"; // URL param that switches debug on
var FORCE_DEBUG = false;
var DEBUG = FORCE_DEBUG ||
new RegExp("[?&]" + DEBUG_PARAM + "=1(?:&|$)").test(window.location.search);
/* ─── CSS property names (match the companion CSS) ─────────────────────── */
var BG_OPACITY_PROP = "--tt-header-bg-opacity";
/* ─── INIT ─────────────────────────────────────────────────────────────── */
var root = document.documentElement;
/* ─── DEBUG HUD ─────────────────────────────────────────────────────────
A small fixed panel showing what the script sees. Created only when
DEBUG is true; zero footprint in production. */
var hud = null;
function ensureHud(){
if (!DEBUG || hud) return;
hud = document.createElement("div");
hud.setAttribute("aria-hidden", "true");
hud.style.cssText =
"position:fixed;left:8px;bottom:8px;z-index:99999;max-width:80vw;" +
"background:rgba(0,0,0,.85);color:#0f0;font:12px/1.5 monospace;" +
"padding:8px 10px;border-radius:6px;pointer-events:none;white-space:pre;";
document.body.appendChild(hud);
}
function updateHud(info){
if (!DEBUG) return;
ensureHud();
hud.textContent =
"TT Header Tint — debug\n" +
"signal sections found: " + info.count + "\n" +
"active: " + info.active + "\n" +
"color read: " + info.color + "\n" +
"triple set: " + info.triple + "\n" +
"transparent: " + info.transparent + "\n" +
"fg: " + info.fg + " via " + fgPath + "\n" +
"sentinel: " + SENTINEL_POS + "\n" +
"header: " + (typeof header !== "undefined" && header ? HEADER_SELECTOR + " \u2713" : HEADER_SELECTOR + " NOT FOUND") +
" · rgb var: " + RGB_VAR;
}
var header = document.querySelector(HEADER_SELECTOR);
if (!header){
// Build the HUD even on failure so the problem is visible, not silent.
updateHud({ count: document.getElementsByClassName(SIGNAL_CLASS).length,
active: "—", color: "—", triple: "—",
transparent: false, fg: "—" });
if (DEBUG) console.warn("[TT_Header_Tint] header not found for selector: " + HEADER_SELECTOR);
return;
}
/* The single sentinel line, pinned inside the header. */
var sentinel = document.createElement("span");
sentinel.className = "tt-header-sentinel";
sentinel.setAttribute("aria-hidden", "true");
if (getComputedStyle(header).position === "static") header.style.position = "relative";
header.appendChild(sentinel);
function placeSentinel(){
var v = String(SENTINEL_POS).trim().toLowerCase();
var topCss;
if (v === "top") topCss = "0";
else if (v === "middle") topCss = "50%";
else if (v === "bottom") topCss = "100%";
else if (v.slice(-2) === "px") topCss = parseFloat(v) + "px";
else { var n = parseFloat(v); topCss = (isNaN(n) ? 100 : Math.max(0, Math.min(100, n))) + "%"; }
if (DEBUG){
sentinel.style.cssText =
"position:absolute;left:0;right:0;height:3px;margin-top:-1px;pointer-events:none;" +
"top:" + topCss + ";background:#ff00d4;box-shadow:0 0 0 1px #fff,0 0 8px #ff00d4;z-index:5;";
} else {
sentinel.style.cssText =
"position:absolute;left:0;width:1px;height:1px;pointer-events:none;top:" + topCss + ";";
}
}
placeSentinel();
/* Cache the signal sections (their set rarely changes). */
var sections = [];
function collectSections(){
sections = Array.prototype.slice.call(
document.getElementsByClassName(SIGNAL_CLASS));
}
/* Resolve stacking: nearest explicit z-index wins; equal/auto falls back
to DOM order (later = painted on top), matching the browser. */
function stackRank(el){
var node = el;
while (node && node !== document.body){
var zi = parseInt(getComputedStyle(node).zIndex, 10);
if (!isNaN(zi)) return zi;
node = node.parentElement;
}
return 0;
}
/* Which section spans the sentinel line; topmost wins. */
function pick(){
var lineY = sentinel.getBoundingClientRect().top;
var best = null, bestZ = -Infinity, bestOrder = -1;
for (var i = 0; i < sections.length; i++){
var r = sections[i].getBoundingClientRect();
if (r.top <= lineY && r.bottom >= lineY){
var z = stackRank(sections[i]);
if (z > bestZ || (z === bestZ && i > bestOrder)){
best = sections[i]; bestZ = z; bestOrder = i;
}
}
}
return best;
}
/* ─── Adaptive foreground (WCAG contrast) ──────────────────────────────── */
/* Resolve any color string (incl. var(--x)) to [r,g,b] 0–255 by letting
the browser parse it on a hidden probe element. */
var probe = document.createElement("span");
probe.style.cssText = "position:absolute;left:-9999px;width:0;height:0;";
document.body.appendChild(probe);
function toRGB(colorStr){
probe.style.color = "";
probe.style.color = colorStr;
var c = getComputedStyle(probe).color;
var m = c.match(/\d+(\.\d+)?/g);
return m ? [ +m[0], +m[1], +m[2] ] : [0, 0, 0];
}
function toTriple(colorStr){
var r = toRGB(colorStr);
return r[0] + ", " + r[1] + ", " + r[2]; // "R, G, B" for --color-the-rgb
}
/* ─── Background color interpolation ───────────────────────────────────
A plain RGB triple cannot be CSS-transitioned, so we tween it ourselves
with rAF and write each intermediate triple to RGB_VAR. easeInOutCubic
matches the cubic-bezier(.4,0,.2,1) feel used elsewhere. */
var curRGB = null; // current displayed [r,g,b]
var fadeRAF = null;
function easeInOut(t){ return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2, 3)/2; }
function setTripleNow(rgb){
root.style.setProperty(RGB_VAR, Math.round(rgb[0]) + ", " + Math.round(rgb[1]) + ", " + Math.round(rgb[2]));
}
function fadeTo(targetRGB){
if (fadeRAF){ cancelAnimationFrame(fadeRAF); fadeRAF = null; }
if (!curRGB){ curRGB = targetRGB.slice(); setTripleNow(curRGB); return; }
var fromRGB = curRGB.slice();
var start = null;
function step(ts){
if (start === null) start = ts;
var t = Math.min(1, (ts - start) / FADE_MS);
var e = easeInOut(t);
var now = [
fromRGB[0] + (targetRGB[0] - fromRGB[0]) * e,
fromRGB[1] + (targetRGB[1] - fromRGB[1]) * e,
fromRGB[2] + (targetRGB[2] - fromRGB[2]) * e
];
setTripleNow(now);
curRGB = now;
if (t < 1){ fadeRAF = requestAnimationFrame(step); }
else { curRGB = targetRGB.slice(); fadeRAF = null; }
}
fadeRAF = requestAnimationFrame(step);
}
/* ─── Foreground: forced style rule + coordinator sync ─────────────────
A bare class loses to Bricks's more-specific nav color rules (confirmed
by inspecting: disabling Bricks's rule let the class through). So we
inject ONE <style> rule that forces color on .tt-header-fg-adaptive and
its a/svg descendants with !important. Coloring descendants matters —
nav text is inside child <a>s and the hamburger icon is an inner <svg>,
so coloring only the tagged container wouldn't reach them.
The logo is handled separately: after forcing the nav color we call
coordinator.syncNavColor(), and the coordinator re-reads the nav's
computed color and propagates it to the logo (which uses
fill:currentColor). No stroke, no direct logo manipulation. */
var fgPath = "init"; // for the HUD
var fgStyle = document.createElement("style");
fgStyle.id = "tt-header-fg-rule";
document.head.appendChild(fgStyle);
function writeFgRule(rgb){
var sel = "." + FG_CLASS;
var c = "rgb(" + Math.round(rgb[0]) + ", " + Math.round(rgb[1]) + ", " + Math.round(rgb[2]) + ")";
fgStyle.textContent =
sel + ", " + sel + " a, " + sel + " button, " + sel + " svg {" +
" color:" + c + " !important; fill:" + c + " !important; }";
}
function syncCoordinator(){
var coord = window[COORDINATOR];
if (coord && typeof coord.syncNavColor === "function"){ coord.syncNavColor(); return true; }
return false;
}
/* Foreground interpolation. A CSS transition won't fire on an !important
value injected via a stylesheet swap, so we tween the color ourselves
and rewrite the rule each frame — then sync the coordinator so the logo
follows. easeInOut matches the cubic-bezier feel used elsewhere. */
var curFg = null; // current displayed [r,g,b], or null when cleared
var fgFadeRAF = null;
function fgFadeTo(targetRGB){
if (fgFadeRAF){ cancelAnimationFrame(fgFadeRAF); fgFadeRAF = null; }
if (!curFg){ curFg = targetRGB.slice(); writeFgRule(curFg); syncCoordinator(); return; }
var fromRGB = curFg.slice();
var start = null;
function step(ts){
if (start === null) start = ts;
var t = Math.min(1, (ts - start) / FADE_MS);
var e = easeInOut(t);
var now = [
fromRGB[0] + (targetRGB[0] - fromRGB[0]) * e,
fromRGB[1] + (targetRGB[1] - fromRGB[1]) * e,
fromRGB[2] + (targetRGB[2] - fromRGB[2]) * e
];
writeFgRule(now);
curFg = now;
if (t < 1){ fgFadeRAF = requestAnimationFrame(step); }
else { curFg = targetRGB.slice(); fgFadeRAF = null; syncCoordinator(); }
}
fgFadeRAF = requestAnimationFrame(step);
}
function pushForeground(rgb){
fgFadeTo(rgb);
fgPath = "forced rule (faded)" + (window[COORDINATOR] ? " + syncNavColor" : "");
}
function clearForeground(){
if (fgFadeRAF){ cancelAnimationFrame(fgFadeRAF); fgFadeRAF = null; }
curFg = null;
fgStyle.textContent = ""; // empty → Bricks's own color returns
fgPath = syncCoordinator() ? "cleared + syncNavColor" : "cleared";
}
function lum(rgb){
var a = rgb.map(function(v){
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
}
function contrast(c1, c2){
var l1 = lum(c1), l2 = lum(c2);
var hi = Math.max(l1, l2), lo = Math.min(l1, l2);
return (hi + 0.05) / (lo + 0.05);
}
function applyForeground(tintStr){
var bg = toRGB(tintStr);
var cl = contrast(toRGB(FG_LIGHT), bg);
var cd = contrast(toRGB(FG_DARK), bg);
var pickLight = cl >= cd;
var chosen = pickLight ? FG_LIGHT : FG_DARK;
// Resolve to concrete RGB so we can interpolate it frame-by-frame.
pushForeground(toRGB(chosen));
if (DEBUG && Math.max(cl, cd) < 4.5){
console.warn("[TT_Header_Tint] Neither foreground clears WCAG AA (4.5:1) on tint " +
tintStr + " — light " + cl.toFixed(2) + ":1, dark " + cd.toFixed(2) + ":1.");
}
return pickLight ? "light" : "dark";
}
/* ─── Apply ────────────────────────────────────────────────────────────── */
/* We only touch the DOM when the active section (or its transparent flag)
changes. When nothing is active we REMOVE our overrides so the header's
own Appearance CSS shows through untouched. */
var lastState = "init";
var lastFg = "—";
function apply(){
var el = pick();
var color = el ? getComputedStyle(el).backgroundColor : null;
var transparent = !!(el && el.hasAttribute(TRANSPARENT_ATTR));
var state = el ? (color + "|" + transparent) : null;
// HUD reflects current reading on EVERY call, even if nothing changed,
// so you can scroll and watch live. Built only when DEBUG is true.
if (DEBUG){
var label = "none (no section under line)";
if (el){
label = (el.id ? "#" + el.id : el.tagName.toLowerCase()) +
(el.className ? "." + String(el.className).trim().split(/\s+/).join(".") : "");
}
updateHud({
count: sections.length,
active: label,
color: color || "—",
triple: color ? toTriple(color) : "—",
transparent: transparent,
fg: lastFg
});
}
if (state === lastState) return;
lastState = state;
if (!el){
// No signal section under the line — clear everything we set so the
// header's own Appearance CSS shows through untouched.
if (fadeRAF){ cancelAnimationFrame(fadeRAF); fadeRAF = null; }
curRGB = null;
root.style.removeProperty(RGB_VAR);
root.style.removeProperty(BG_OPACITY_PROP);
clearForeground();
lastFg = "—";
return;
}
// Fade the gradient color smoothly to the section's color.
fadeTo(toRGB(color));
lastFg = applyForeground(color);
// Transparent (image) section: drop the backdrop opacity var to 0;
// .tt-header-bg-adaptive consumes it and CSS gives the 600ms curve.
// Foreground still contrasts against the section's background-color.
root.style.setProperty(BG_OPACITY_PROP, transparent ? "0" : "1");
}
/* rAF-throttled scroll; nothing runs while idle. */
var ticking = false;
function onScroll(){
if (ticking) return;
ticking = true;
requestAnimationFrame(function(){ apply(); ticking = false; });
}
collectSections();
apply();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", function(){
collectSections(); placeSentinel(); apply();
}, { passive: true });
if (window.ResizeObserver){
var ro = new ResizeObserver(function(){ apply(); });
ro.observe(header);
}
} /* end boot() */
if (document.readyState === "loading"){
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();