fix(hud): anchor lower-left readouts to the attribution row

The credits row takes its position from --gev-credits-bottom, and the
lower-left readout of Minimal, Tactical, Cyber and Cockpit sits
--gev-credits-reserve (2.75rem) above it: 16px over the 28px logo row.
Tactical's box overlapped the row by 4px, and Cockpit's corner offset
outranked every layout and put the readout 21-32px into it. Cockpit now
scales the readout from its bottom edge so it stays below the context
window.

Remove the overridden Minimal offsets in overlays.css, command-dock.css
and command-dock-compact.css so one rule owns each position.
This commit is contained in:
German Khokhlov committed 2026-11-06 21:31:56 +04:00
1 parent d61a9225eb
commit 055709c12b
8 files changed
+77 -39

No files matched your search

+19 -2
View File
@@ -299,9 +299,26 @@ function resolve(candidates, prop, width, label) {
return winner;
}
/**
* Inline a custom property declared once, on a bare :root, so an offset written
* through it evaluates to the length it stands for. Anything else stays a
* var() for toPx to vet.
*/
function inlineRootVars(value) {
const inlined = value.replace(/var\((--[\w-]+)\)/g, (match, name) => {
const declarations = RULES.flatMap((rule) =>
rule.decls.filter((decl) => decl.prop === name).map((decl) => ({ rule, decl })));
if (declarations.length !== 1) return match;
const [{ rule, decl }] = declarations;
if (rule.media.length || rule.parts.join() !== ':root') return match;
return decl.value.replace(/^calc\((.*)\)$/, '$1');
});
return inlined === value || /^calc\(/.test(inlined) ? inlined : `calc(${inlined})`;
}
/** Evaluate a length. Anything the model cannot resolve exactly fails loudly. */
function toPx(value, viewportHeight, where) {
const trimmed = value.trim();
const trimmed = inlineRootVars(value.trim());
const inner = /^calc\(/.test(trimmed) ? trimmed.slice(5, -1) : trimmed;
assert.doesNotMatch(inner, /\bcalc\(/, `nested calc() in ${where}: "${value}"`);
assert.doesNotMatch(inner, /\b(min|max|clamp|env|attr|round|mod)\(/, `unmodelled function in ${where}: "${value}"`);
@@ -579,7 +596,7 @@ test('the dock anchor changes at 720px — the 2vh cancellation is band-limited'
assert.equal(resolve(['#command-dock'], 'bottom', 800, 'dock').decl.value, '2vh');
assert.equal(resolve(['#command-dock'], 'bottom', 720, 'dock').decl.value, '8px');
assert.equal(
resolve(CREDIT_SELECTORS, 'bottom', 720, 'credit').decl.value,
inlineRootVars(resolve(CREDIT_SELECTORS, 'bottom', 720, 'credit').decl.value),
'calc(2vh + 5rem)',
'the credit keeps its 2vh base below 720px — that asymmetry is the whole hazard',
);
+37 -13
View File
@@ -429,24 +429,48 @@ test('Cyber dock pins escape the tray mask and its decoration overrides the lega
assert.match(decoration, /pointer-events:\s*none;/);
});
test('Cyber lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
const telemetry = cssRuleBody(
cyberStyles,
/#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/,
);
const offset = telemetry.match(/bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/);
assert.ok(offset, 'telemetry must retain the attribution viewport anchor');
test('lower-left telemetry tracks the credit anchor with clearance for its logo row', () => {
const credits = read('./styles/command-dock-trays.css');
const anchor = cssRuleBody(credits, /:root/);
assert.match(anchor, /--gev-credits-bottom:\s*calc\(2vh \+ 5rem\);/);
const reserve = anchor.match(/--gev-credits-reserve:\s*([\d.]+)rem;/);
assert.ok(
Number(reserve?.[1]) >= 2.75,
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
);
const creditOffsets = [
...credits.matchAll(
/#cesium-credits\s*\{[^}]*bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/g,
),
...credits.matchAll(/#cesium-credits\s*\{[^}]*bottom:\s*([^;]+);/g),
];
assert.ok(creditOffsets.length > 0);
for (const credit of creditOffsets) {
assert.ok(
Number(offset[1]) - Number(credit[1]) >= 2.75,
'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size',
assert.equal(credit[1], 'var(--gev-credits-bottom)');
}
const reserved =
/bottom:\s*calc\(var\(--gev-credits-bottom\) \+ var\(--gev-credits-reserve\)\);/;
assert.match(
cssRuleBody(cyberStyles, /#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/),
reserved,
);
assert.match(
cssRuleBody(
credits,
/#intel-hud\[data-variant='tactical'\] \.hud-bottom-left,\s*#intel-hud\[data-variant='minimal'\] \.hud-bottom-left/,
),
reserved,
);
// Cockpit's corner offset outranks the layouts' rules, so it reserves too.
assert.match(
read('./styles/cockpit.css'),
new RegExp(
`\\nbody\\.cockpit-mode #intel-hud \\.hud-bottom-left\\s*\\{[^}]*${reserved.source}`,
),
);
// One rule owns each layout's lower-left offset; import order must not decide it.
for (const file of ['overlays.css', 'command-dock.css', 'command-dock-compact.css']) {
assert.doesNotMatch(
read(`./styles/${file}`),
/\[data-variant='\w+'\] \.hud-bottom-left\s*\{[^}]*bottom:/,
file,
);
}
assert.doesNotMatch(cyberStyles, /\.hud-bottom-left\s*\{\s*bottom:\s*60px;/);
+7 -1
View File
@@ -2059,10 +2059,16 @@ body.cockpit-mode #intel-hud .hud-top-left,
body.cockpit-mode #intel-hud .hud-top-right {
top: clamp(104px, 13vh, 138px);
}
body.cockpit-mode #intel-hud .hud-bottom-left,
body.cockpit-mode #intel-hud .hud-bottom-right {
bottom: clamp(84px, 11vh, 118px);
}
/* The lower-left readout sits over the attribution row, which keeps its map
position in Cockpit. Scaling from its bottom edge keeps it on that reserve
and below the context window's rail. */
body.cockpit-mode #intel-hud .hud-bottom-left {
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
transform-origin: left bottom;
}
body.cockpit-mode #intel-hud .hud-summary-wrap {
max-width: min(34vw, 380px);
}
-4
View File
@@ -380,10 +380,6 @@ body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
bottom: calc(2vh + 4.7rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 5.4rem);
}
@media (max-width: 900px) {
#command-dock #location-bar .location-toolbar-label {
overflow: hidden;
+12 -5
View File
@@ -448,6 +448,14 @@
letter-spacing: 0.035rem;
}
/* The attribution row's anchor above the command dock. HUD readouts over the
row sit the reserve above it: the 28px logo row, baseline descent and 12px
clearance at the 16px root size. */
:root {
--gev-credits-bottom: calc(2vh + 5rem);
--gev-credits-reserve: 2.75rem;
}
/* Keep the complete inline attribution row above the command dock. */
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits,
body:not(.ui-clean-view):not(.recording-mode):has(
@@ -455,21 +463,20 @@ body:not(.ui-clean-view):not(.recording-mode):has(
)
#cesium-credits {
right: auto;
bottom: calc(2vh + 5rem);
bottom: var(--gev-credits-bottom);
left: 1.5rem;
width: auto !important;
max-width: calc(100vw - 3rem);
text-align: left;
}
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='tactical'] .hud-bottom-right {
bottom: calc(2vh + 6.5rem);
}
/* Clear the attribution row, whose logo stands about 1.75rem tall. */
#intel-hud[data-variant='tactical'] .hud-bottom-left,
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 7.25rem);
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
}
@media (max-width: 900px) {
@@ -478,7 +485,7 @@ body:not(.ui-clean-view):not(.recording-mode):has(
#intel-hud[data-variant='minimal'].active
)
#cesium-credits {
bottom: calc(2vh + 5rem);
bottom: var(--gev-credits-bottom);
left: 0.75rem;
max-width: calc(100vw - 1.5rem);
}
-8
View File
@@ -243,10 +243,6 @@ body.recording-mode #cesium-credits {
left: 36px;
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 7.5rem);
}
@media (max-width: 980px) {
#command-dock {
grid-template-areas:
@@ -273,10 +269,6 @@ body.recording-mode #cesium-credits {
body:not(.ui-clean-view):not(.recording-mode) #cesium-credits {
bottom: calc(2vh + 9.5rem);
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: calc(2vh + 12rem);
}
}
@media (max-width: 720px) {
+2 -2
View File
@@ -1605,9 +1605,9 @@
}
/* Attribution's logo row extends above its text baseline. Reserve the whole
row plus a gap above the dock-relative 2vh + 5rem credit position. */
row plus a gap above the dock-relative credit anchor. */
#intel-hud[data-variant='cyber'] .hud-bottom-left {
bottom: calc(2vh + 7.75rem);
bottom: calc(var(--gev-credits-bottom) + var(--gev-credits-reserve));
}
#intel-hud[data-variant='cyber'] .hud-corner {
-4
View File
@@ -306,10 +306,6 @@
top: 88px;
}
#intel-hud[data-variant='minimal'] .hud-bottom-left {
bottom: 12vh;
}
#intel-hud[data-variant='minimal'] .hud-summary-wrap {
max-width: 280px;
}