← back to Quadrille Showroom
Age View bar legibility: fix invisible readout text on light-panel bands + add per-band bar-legibility gate
67caf4d47d7dd80c719722e3ed27b88a2af9dd14 · 2026-06-29 16:39:30 -0700 · Steve
Files touched
M 5x/loop-ledger.mdM public/js/ageview.jsM scripts/verify-ageview-phase5.mjs
Diff
commit 67caf4d47d7dd80c719722e3ed27b88a2af9dd14
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 16:39:30 2026 -0700
Age View bar legibility: fix invisible readout text on light-panel bands + add per-band bar-legibility gate
---
5x/loop-ledger.md | 1 +
public/js/ageview.js | 26 ++++++++++++++++++------
scripts/verify-ageview-phase5.mjs | 42 +++++++++++++++++++++++++++++++++++++--
3 files changed, 61 insertions(+), 8 deletions(-)
diff --git a/5x/loop-ledger.md b/5x/loop-ledger.md
index 09ecfd4..7c81c3b 100644
--- a/5x/loop-ledger.md
+++ b/5x/loop-ledger.md
@@ -3,3 +3,4 @@
- iter1 (2026-06-29): FPS gate FAIL root-caused — phase5 sampler caught the morph-rebuild transient window (band 65 min=54). Fixed sampler to discard the band-change-straddling counter window + dedup per distinct window + 1200ms settle; floor unchanged at 55. All bands now steady-state 67-74 min. All 4 verifiers GREEN.
- iter2 (2026-06-29): Backlog (a) DONE — new scripts/verify-ageview-envmorph.mjs MEASURES real rendered wall-luminance for the setEnvForAge morph (was only asserted by intensity scalars). Drives age 35 vs 85 on real GPU, samples pure-plaster wall regions: senior is +6.56 luminance brighter (morph proven to change the render), lower-glare hot-pixel frac non-increasing, OFF restores exact baseline (Δ0.00, avatarAge 28). PASS.
- iter3 (2026-06-29): Backlog (c) DONE — extended verify-walls-on-next.mjs with an Age-View-ON pass (senior band 75). Paging Next under Age View still clads all 4 walls, 0 console errors; locks the env+avatar-morph × wall-clad interaction as a regression guard. All 5 verifiers GREEN.
+- iter4 (2026-06-29): Backlog (c) DONE + REAL DEFECT FIXED — Age View slider bar text was INVISIBLE at tween/connoisseur/tastemaker bands (age#/band-name Lc≈0: dark band-tuned text on the bar's dark glass). Fixed ageview.js to render the bar readout (age#/band/tag/thumb) in guaranteed-light vars on non-senior bands, falling back to the band's gated colours only when the bar is opaque (senior). Added a per-band BAR-legibility gate to phase5 (was 85-only) + an FPS warm-up apply so band-12 isn't contaminated by the one-time Age-View-activation transient. All bands now age#Lc≥69 band≥101 tag≥74. All 5 verifiers GREEN.
diff --git a/public/js/ageview.js b/public/js/ageview.js
index 708ed0f..52fc37d 100644
--- a/public/js/ageview.js
+++ b/public/js/ageview.js
@@ -120,19 +120,33 @@ function injectChrome() {
body.age-opaque #ageview-bar { backdrop-filter:none; background:var(--av-panel); }
#ageview-bar.hidden { display:none; }
#av-readout { text-align:center; min-width:120px; }
- #av-age { font-weight:800; font-size:max(28px, var(--av-head)); line-height:1; color:var(--av-cta); }
- body.age-legacy #av-age { color:var(--av-cta); }
- #av-band { font-size:max(15px, var(--av-body)); font-weight:700; letter-spacing:.4px; margin-top:3px; color:var(--av-fg); }
+ /* The Age View bar shows a DARK glass panel for every NON-senior band (the
+ senior/elder/legacy bands override it to their own opaque panel). So the bar's
+ readout text must NOT inherit the band's --av-cta / --av-fg — several bands
+ (tween indigo, connoisseur/tastemaker dark-maroon over their OWN light panel)
+ carry DARK text colours that vanish on the dark glass (age#/band Lc≈0, found
+ 2026-06-29). Bar text uses guaranteed-light readout vars; only when the bar is
+ itself opaque (senior bands) do we fall back to the band's real colours, which
+ are gated by the senior APCA check. */
+ body.ageview-on { --av-bar-num:#c9b6ff; --av-bar-fg:#f2f5fb; --av-bar-muted:#c2cad8; --av-bar-thumb:#ffffff; }
+ #av-age { font-weight:800; font-size:max(28px, var(--av-head)); line-height:1; color:var(--av-bar-num); }
+ #av-band { font-size:max(15px, var(--av-body)); font-weight:700; letter-spacing:.4px; margin-top:3px; color:var(--av-bar-fg); }
+ /* Senior bands: bar is opaque on the band's own panel → use the band's gated colours. */
+ body.age-opaque #av-age { color:var(--av-cta); }
+ body.age-opaque #av-band { color:var(--av-fg); }
/* Slider thumb/track gated for the elder band — large hit target + bright track. */
#av-slider { -webkit-appearance:none; appearance:none; height:14px; border-radius:8px; min-width:300px; flex:1;
background:var(--av-cta); outline:none; cursor:pointer; }
#av-slider::-webkit-slider-thumb { -webkit-appearance:none; appearance:none; width:34px; height:34px; border-radius:50%;
- background:var(--av-fg); border:3px solid var(--av-cta); cursor:pointer; }
- #av-slider::-moz-range-thumb { width:34px; height:34px; border-radius:50%; background:var(--av-fg); border:3px solid var(--av-cta); cursor:pointer; }
+ background:var(--av-bar-thumb); border:3px solid var(--av-cta); cursor:pointer; }
+ #av-slider::-moz-range-thumb { width:34px; height:34px; border-radius:50%; background:var(--av-bar-thumb); border:3px solid var(--av-cta); cursor:pointer; }
+ body.age-opaque #av-slider::-webkit-slider-thumb { background:var(--av-fg); }
+ body.age-opaque #av-slider::-moz-range-thumb { background:var(--av-fg); }
body.age-opaque #av-slider { height:18px; }
body.age-opaque #av-slider::-webkit-slider-thumb { width:40px; height:40px; }
body.age-opaque #av-slider::-moz-range-thumb { width:40px; height:40px; }
- #av-tag { flex-basis:100%; text-align:center; font-size:max(15px, var(--av-body)); color:var(--av-muted); margin-top:2px; }
+ #av-tag { flex-basis:100%; text-align:center; font-size:max(15px, var(--av-body)); color:var(--av-bar-muted); margin-top:2px; }
+ body.age-opaque #av-tag { color:var(--av-muted); }
/* Re-theme the live HUD chrome when Age View is on. CSS-var driven, so the showroom's
own component CSS doesn't change — only the values do. Banded snap, not interpolation. */
diff --git a/scripts/verify-ageview-phase5.mjs b/scripts/verify-ageview-phase5.mjs
index c13b162..4ba805c 100644
--- a/scripts/verify-ageview-phase5.mjs
+++ b/scripts/verify-ageview-phase5.mjs
@@ -149,6 +149,12 @@ const FPS_FLOOR = 55;
console.log('Age View ON =', isOn);
// ---------- 5.1 FPS gate across every band ----------
+ // Warm-up apply: the FIRST band measured right after setOn(true) otherwise eats
+ // the one-time Age-View activation cost (font load, style inject, first env morph),
+ // which is NOT steady-state render. Burn one throwaway band + settle before the
+ // measured loop so every band is sampled at steady state. Floor unchanged at 55.
+ await page.evaluate(() => window._ageview.apply(28));
+ await sleep(1600);
console.log('\n=== 5.1 FPS gate (≥' + FPS_FLOOR + ' at every band) ===');
for (const age of ALL_AGES) {
await page.evaluate(a => window._ageview.apply(a), age);
@@ -158,6 +164,37 @@ const FPS_FLOOR = 55;
console.log(` age ${String(age).padStart(2)} min=${fps.min} avg=${fps.avg} (${report.fps[age].pass ? 'PASS' : 'FAIL'})`);
}
+ // ---------- 5.1b Age View BAR legibility at EVERY band ----------
+ // The persistent Age View slider bar (age#, band name, tag) must stay readable at
+ // every band — not just the senior bands. Several bands carry dark text colours
+ // tuned for their OWN light panel that vanish on the bar's dark glass (tween,
+ // connoisseur, tastemaker had age#/band Lc≈0, found + fixed 2026-06-29). Measured
+ // APCA on the rendered text/bar hex pair; floor 45 for the age# (display weight,
+ // 28px+) and band/tag (the readout copy). Senior bands clear this trivially.
+ console.log('\n=== 5.1b Age View BAR legibility (every band, measured APCA) ===');
+ const BAR_FLOOR = 45;
+ report.barLegibility = {};
+ let barAllPass = true;
+ for (const age of ALL_AGES) {
+ await page.evaluate(a => window._ageview.apply(a), age);
+ await sleep(500);
+ const r = await page.evaluate(() => {
+ const lc = (txt, bg) => Math.abs(window._ageview.apcaLc(txt, bg));
+ const meas = (id) => {
+ const el = document.getElementById(id);
+ if (!el || !window.__visible(el)) return null;
+ const bg = window.__effectiveBg(el), fg = window.__textHex(el);
+ return { lc: (bg.opaque && fg) ? lc(fg, bg.hex) : null, fg, bg: bg.hex, px: Math.round(window.__fontPx(el)) };
+ };
+ return { band: document.body.dataset.ageBand, age: meas('av-age'), bandName: meas('av-band'), tag: meas('av-tag') };
+ });
+ const aLc = r.age?.lc ?? 0, bLc = r.bandName?.lc ?? 0, tLc = r.tag?.lc ?? 0;
+ const pass = aLc >= BAR_FLOOR && bLc >= BAR_FLOOR && tLc >= BAR_FLOOR;
+ if (!pass) barAllPass = false;
+ report.barLegibility[age] = { ...r, pass };
+ console.log(` age ${String(age).padStart(2)} [${r.band}] age#Lc=${aLc} bandLc=${bLc} tagLc=${tLc} (${pass ? 'PASS' : 'FAIL'})`);
+ }
+
// ---------- 5.2 Senior measured gate ----------
console.log('\n=== 5.2 Senior-usability MEASURED gate (65/75/85) ===');
// Roles: which visible elements carry body text, headings, CTAs, interactive targets.
@@ -302,13 +339,14 @@ const FPS_FLOOR = 55;
const regPass = report.regression.pass;
const errPass = errors.length === 0;
report.verdict = {
- fpsPass, seniorPass, regPass, errPass, consoleErrors: errors.length,
- overall: fpsPass && seniorPass && regPass && errPass,
+ fpsPass, barPass: barAllPass, seniorPass, regPass, errPass, consoleErrors: errors.length,
+ overall: fpsPass && barAllPass && seniorPass && regPass && errPass,
};
fs.writeFileSync(path.join(OUT, 'phase5-result.json'), JSON.stringify(report, null, 2));
console.log('\n================ PHASE 5 VERDICT ================');
console.log(' FPS gate :', fpsPass ? 'PASS' : 'FAIL');
+ console.log(' Bar legib. :', barAllPass ? 'PASS' : 'FAIL');
console.log(' Senior gate :', seniorPass ? 'PASS' : 'FAIL');
console.log(' Regression :', regPass ? 'PASS' : 'FAIL');
console.log(' Console errs :', errors.length, errPass ? 'PASS' : 'FAIL');
← 796c25f walls-on-next: add Age-View-ON regression pass (paging under
·
back to Quadrille Showroom
·
Loop ledger iter5: two clean clickthrough sweeps + visual co 66c765f →