Merge main, carrying the observatory instrument onto its rebuilt chrome

Two HUD redesigns happened in parallel: this branch restyled the old chrome
as a precision observatory instrument, while main rebuilt the chrome against
screenshots of the Star Citizen starmap — a scale ladder, a nameplate, a
readout panel, and two-line labels that say what a thing is, not just what
it is called.

This merge keeps everything main's rebuild learned and says it in this
branch's voice. Kept: the ladder's reachability semantics and test hooks,
the two-line name/kind labels and their shared declutter logic, system-view
body labels, every readout. Restyled: chamfered clip-path panels become
hairline frames with corner-tick brackets; Orbitron is gone and one readout
face carries the hierarchy; the hexagon reticle becomes the same
circle-and-ticks mark the search field wears; focus states move to real
focus-visible outlines; the long uppercase caveat notes drop to sentence
case so they read as sentences.

Two merge-borne fixes along the way: the labels' translate offset moved into
.map-label as a margin (CSS2DRenderer rewrites the inline transform every
frame, the margin is the offset it cannot touch), and the info panel's new
Derived section picked up the horizontal padding it lost when the panel
moved to per-block padding.

Verified: build clean, 496/496 unit, 6/6 e2e (on a free port — 4300 is
occupied on this machine), design detector clean, screenshots reviewed at
1440x900 and 390x844 across all four views plus search states.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-08-17 16:09:33 +02:00
co-authored by Claude Fable 5
91 changed files with 19628 additions and 350 deletions
@@ -2,6 +2,7 @@ import { DecimalPipe } from '@angular/common';
import { Component, input } from '@angular/core';
import { Router } from '@angular/router';
import { PLANET_CLASS_LABELS } from '../../shared/astro/planet-appearance';
import { BodyDetailViewModel } from './body-detail.model';
const KIND_LABELS: Record<BodyDetailViewModel['kind'], string> = {
@@ -78,6 +79,28 @@ const KIND_LABELS: Record<BodyDetailViewModel['kind'], string> = {
</div>
}
</dl>
<p class="border-t border-border/40 px-4 pt-3 pb-1 text-[10px] tracking-[0.16em] text-muted uppercase">Derived</p>
<dl class="divide-y divide-border/25">
<div class="flex items-baseline justify-between gap-4 px-4 py-2">
<dt class="text-[10px] tracking-[0.16em] text-muted uppercase">Class</dt>
<dd class="text-sm">{{ classLabel() }}</dd>
</div>
@if (body().appearance.equilibriumTemperatureK !== null) {
<div class="flex items-baseline justify-between gap-4 px-4 py-2">
<dt class="text-[10px] tracking-[0.16em] text-muted uppercase">Equilibrium temp.</dt>
<dd class="text-sm tabular-nums">{{ body().appearance.equilibriumTemperatureK | number: '1.0-0' }} <span class="text-muted">K</span></dd>
</div>
}
@if (body().appearance.bulkDensityGramsPerCm3 !== null) {
<div class="flex items-baseline justify-between gap-4 px-4 py-2">
<dt class="text-[10px] tracking-[0.16em] text-muted uppercase">Bulk density</dt>
<dd class="text-sm tabular-nums">{{ body().appearance.bulkDensityGramsPerCm3 | number: '1.0-2' }} <span class="text-muted">g/cm³</span></dd>
</div>
}
</dl>
<p class="border-t border-border/40 px-4 py-3 text-[10px] leading-relaxed text-muted">{{ surfaceProvenance() }}</p>
</div>
`,
imports: [DecimalPipe]
@@ -91,6 +114,25 @@ export class InfoPanelComponent {
return KIND_LABELS[this.body().kind];
}
classLabel(): string {
return PLANET_CLASS_LABELS[this.body().appearance.planetClass];
}
/**
* Says plainly which of the two the viewer is looking at. The derived surface is a reasoned
* illustration, and a panel of real measurements sitting next to it is exactly the context in
* which it could be mistaken for another one.
*/
surfaceProvenance(): string {
if (this.body().hasPhotography) {
return 'Surface: NASA/ESA/USGS photography.';
}
const temperature = this.body().appearance.equilibriumTemperatureK;
return temperature === null
? 'Surface illustrated from this body’s measured size and mass. Its host star is not in the catalogue, so no temperature could be derived. Not an observation — no image of this world exists.'
: 'Surface illustrated from the measurements above — size, density and the temperature derived from its star’s output and its orbit. Not an observation — no image of this world exists.';
}
goBack(): void {
void this.router.navigate(['/']);
}