feat: overhead gantry top bar with a docking sign window

The top bar becomes a steel gantry. Its centre window is a mask cut-out the
3D exit sign docks into; between signs the window shows the dash read-out.
The drive controls move into the right bay, and the reading viewport is
clipped at the beam, so the empty band under the bar is gone.
This commit is contained in:
2026-10-10 22:20:48 +03:00
parent 32eb3e4d40
commit 0ae27168cd
16 changed files with 819 additions and 398 deletions

517
src/styles/gantry.css Normal file
View File

@@ -0,0 +1,517 @@
/* ==========================================================================
Round 5: the overhead gantry.
The top bar is a steel gantry spanning the road. Its sign window is a real
cut-out: the WebGL sign docks in it, and between signs the dash read-out
(JourneyHud) shows through it. The drive controls live in its right bay,
so nothing floats over the reading column. Content starts at the beam's
lower edge (sign-stage.ts clips the reading viewport there).
========================================================================== */
:root {
--gantry-h: 72px;
--window-w: 256px;
--window-h: 64px;
--bay-l: 232px;
--bay-r: 232px;
--window-x: calc(50% - var(--window-w) / 2);
/* mask-position percentages resolve against (box - layer), so the cut-out
needs its own x: 50% centres it, 100% - n puts its right edge n in. */
--window-mask-x: 50%;
--window-y: calc((var(--gantry-h) - var(--window-h)) / 2);
--gantry-steel-1: #1d1230;
--gantry-steel-2: #120c22;
--gantry-rail: rgb(214 140 222 / 30%);
--site-navigation-block-size: var(--gantry-h);
}
/* Mid widths: the window sits beside the controls bay. */
@media (min-width: 768px) and (max-width: 1279px) {
:root {
--window-w: 224px;
--window-h: 56px;
--bay-l: 12px;
--bay-r: 112px;
--window-x: calc(100% - var(--bay-r) - var(--window-w) - 6px);
--window-mask-x: calc(100% - var(--bay-r) - 6px);
}
}
@media (max-width: 767px) {
:root {
--gantry-h: 56px;
--window-h: 40px;
--window-w: min(160px, calc(100vw - 236px));
--bay-l: 116px;
--bay-r: 104px;
--window-x: var(--bay-l);
--window-mask-x: var(--bay-l);
}
}
/* ---------- The beam ---------- */
html .site-navigation,
html .site-navigation[data-enhanced='true'],
html .site-navigation[data-enhanced='true'][data-docked='true'] {
inset: 0 0 auto;
min-block-size: var(--gantry-h);
block-size: var(--gantry-h);
transform: none;
border: 0;
background: none;
box-shadow: none;
}
html .site-navigation::before,
html .site-navigation[data-enhanced='true']::before,
html .site-navigation[data-enhanced='true'][data-docked='true']::before {
inset: 0;
transform: none;
border: 0;
border-radius: 0;
/* Rivet rails top and bottom, brushed steel between, a cut-out window. */
background:
repeating-linear-gradient(90deg, rgb(255 190 240 / 26%) 0 2px, transparent 2px 26px) 0 5px / 100% 2px no-repeat,
repeating-linear-gradient(90deg, rgb(255 190 240 / 18%) 0 2px, transparent 2px 26px) 13px calc(100% - 6px) / 100% 2px no-repeat,
linear-gradient(180deg, rgb(255 255 255 / 5%), transparent 30% 70%, rgb(0 0 0 / 30%)),
linear-gradient(100deg, var(--gantry-steel-1), var(--gantry-steel-2) 55%, #170f2a);
box-shadow:
0 1px 0 var(--gantry-rail),
0 14px 34px -8px rgb(6 1 16 / 70%);
-webkit-mask:
linear-gradient(#000 0 0),
linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat;
-webkit-mask-composite: xor;
mask:
linear-gradient(#000 0 0),
linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat;
mask-composite: exclude;
}
/* Brand plate in the left bay (decorative; the h1 carries the name). */
html .site-navigation::after {
content: 'LUCA P\00CESLARU' / '';
position: absolute;
inset-block: 0;
inset-inline-start: 22px;
display: flex;
align-items: center;
gap: 10px;
color: #f4e6ff;
font: 680 0.74rem/1 var(--mono);
letter-spacing: 0.24em;
pointer-events: none;
white-space: nowrap;
}
@media (max-width: 1279px) {
html .site-navigation::after { display: none; }
}
/* ---------- Exits on the beam ---------- */
@media (min-width: 768px) {
html .site-navigation[data-enhanced] .site-navigation__disclosure,
html .site-navigation .site-navigation__disclosure {
inline-size: 100%;
max-inline-size: none;
block-size: 100%;
margin: 0;
padding-inline: var(--bay-l) calc(var(--bay-r) + var(--window-w) + 18px);
}
html .site-navigation ul {
grid-template-columns: repeat(8, minmax(0, 1fr));
grid-auto-rows: var(--gantry-h);
gap: 0;
padding: 0;
block-size: 100%;
}
html .site-navigation ul::before,
html .site-navigation ul::after {
display: none;
}
html .site-navigation a {
position: relative;
block-size: 100%;
flex-direction: column;
gap: 5px;
padding-inline: 2px;
border-radius: 0;
background: none;
color: #bfaed3;
font: 560 0.72rem/1.1 var(--sans);
letter-spacing: 0.01em;
text-align: center;
}
html .site-navigation a::before {
content: counter(exit, decimal-leading-zero) / '';
margin: 0;
color: color-mix(in srgb, var(--rose, #ff83c2) 70%, #8f7aa6);
font: 650 0.6rem/1 var(--mono);
letter-spacing: 0.14em;
opacity: 0.85;
}
html .site-navigation a:hover,
html .site-navigation a:focus-visible {
background: linear-gradient(180deg, transparent, rgb(255 131 194 / 10%));
color: #fff4fb;
}
html .site-navigation a[aria-current='page'] {
color: #fff4fb;
background: linear-gradient(180deg, transparent 20%, rgb(255 214 147 / 10%));
}
html .site-navigation a[aria-current='page']::before {
color: var(--focus);
opacity: 1;
}
/* Lane lamps under each exit: dim ahead, lit once passed, amber here. */
html .site-navigation li::after {
content: '' / '';
inset: auto 18% 9px;
inline-size: auto;
block-size: 2px;
translate: none;
border: 0;
border-radius: 2px;
background: rgb(198 122 209 / 22%);
box-shadow: none;
transition: background-color 360ms ease, box-shadow 360ms ease, inset 360ms cubic-bezier(0.22, 1, 0.36, 1);
}
html .site-navigation li[data-route='passed']::after {
inline-size: auto;
block-size: 2px;
border: 0;
background: color-mix(in srgb, var(--rose, #ff83c2) 70%, transparent);
}
html .site-navigation li[data-route='here']::after {
inset: auto 10% 9px;
inline-size: auto;
block-size: 3px;
border-radius: 2px;
background: var(--focus);
box-shadow: 0 0 12px rgb(255 214 147 / 70%);
}
}
@media (min-width: 1280px) {
/* Wide: exits 01-04 left of the window, 05-08 right of it. */
html .site-navigation[data-enhanced] .site-navigation__disclosure,
html .site-navigation .site-navigation__disclosure {
padding-inline: var(--bay-l) var(--bay-r);
}
html .site-navigation ul {
grid-template-columns: repeat(4, minmax(0, 1fr)) calc(var(--window-w) + 28px) repeat(4, minmax(0, 1fr));
}
html .site-navigation li:nth-child(5) {
grid-column-start: 6;
}
html .site-navigation a {
font-size: 0.8rem;
}
}
@media (min-width: 768px) and (max-width: 1099px) {
html .site-navigation a::before { display: none; }
html .site-navigation a { font-size: 0.68rem; line-height: 1.05; }
}
/* ---------- The sign window ---------- */
.gantry-window {
position: absolute;
z-index: 1;
inset-block-start: var(--window-y);
inset-inline-start: var(--window-x);
inline-size: var(--window-w);
block-size: var(--window-h);
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
cursor: pointer;
/* A steel lip with a lit inner edge, and depth over the live scene. */
box-shadow:
0 0 0 1px rgb(255 160 220 / 34%),
0 0 0 4px rgb(10 5 20 / 92%),
0 0 0 5px rgb(214 140 222 / 26%),
inset 0 0 18px rgb(6 2 16 / 55%);
transition: box-shadow 320ms ease;
}
.gantry-window::before,
.gantry-window::after {
content: '' / '';
position: absolute;
inline-size: 12px;
block-size: 12px;
border: 2px solid var(--focus);
pointer-events: none;
opacity: 0.75;
transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1), opacity 320ms ease;
}
.gantry-window::before {
inset: -7px auto auto -7px;
border-inline-end: 0;
border-block-end: 0;
}
.gantry-window::after {
inset: auto -7px -7px auto;
border-inline-start: 0;
border-block-start: 0;
}
.gantry-window:hover::before,
.gantry-window:focus-visible::before { transform: translate(-3px, -3px); opacity: 1; }
.gantry-window:hover::after,
.gantry-window:focus-visible::after { transform: translate(3px, 3px); opacity: 1; }
.gantry-window:focus-visible {
outline: 2px solid var(--focus);
outline-offset: 6px;
}
html[data-gantry='docked'] .gantry-window,
html[data-gantry='incoming'] .gantry-window {
box-shadow:
0 0 0 1px rgb(255 214 147 / 70%),
0 0 0 4px rgb(10 5 20 / 92%),
0 0 0 5px rgb(255 214 147 / 35%),
0 0 26px rgb(255 160 220 / 22%),
inset 0 0 18px rgb(6 2 16 / 40%);
}
@media (max-width: 359px) {
.gantry-window { display: none; }
}
/* ---------- The dash read-out, shown through the window ---------- */
html .journey-hud {
position: fixed;
z-index: var(--z-hud);
inset: var(--window-y) auto auto 0;
translate: none;
margin-inline-start: 0;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-rows: 1fr auto;
align-items: center;
column-gap: 9px;
row-gap: 3px;
inline-size: var(--window-w);
max-inline-size: none;
block-size: var(--window-h);
min-block-size: 0;
padding: 7px 11px 6px;
border: 0;
border-radius: 0;
background: linear-gradient(180deg, rgb(20 10 34 / 94%), rgb(12 7 24 / 94%));
box-shadow: none;
color: #cbb9de;
letter-spacing: 0.06em;
pointer-events: none;
transition: opacity 260ms ease, transform 360ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* The window's left edge, without reading the nav's own custom properties. */
@media (min-width: 1280px) {
html .journey-hud { inset-inline-start: calc(50% - var(--window-w) / 2); }
}
@media (min-width: 768px) and (max-width: 1279px) {
html .journey-hud { inset-inline-start: calc(100% - var(--bay-r) - var(--window-w) - 6px); }
}
@media (max-width: 767px) {
html .journey-hud { inset-inline-start: var(--bay-l); }
}
html .journey-hud[data-exit]::before {
grid-column: 1;
grid-row: 1;
align-self: center;
font-size: 0.6rem;
}
html .journey-hud__section {
grid-column: 2;
grid-row: 1;
overflow: hidden;
max-inline-size: none;
color: #fff4fb;
font: 620 0.98rem/1 var(--sans);
letter-spacing: -0.01em;
text-overflow: ellipsis;
white-space: nowrap;
}
html .journey-hud__section::before { display: none; }
html .journey-hud[data-clock]::after {
grid-column: 3;
grid-row: 1;
padding-inline-start: 0;
border: 0;
color: #a998bd;
font: 560 0.58rem/1 var(--mono);
}
html .journey-hud__route {
grid-column: 1 / -1;
grid-row: 2;
gap: 0.35rem;
overflow: hidden;
color: #a998bd;
font: 560 0.56rem/1 var(--mono);
letter-spacing: 0.12em;
white-space: nowrap;
}
html .journey-hud__route span { font-size: inherit; color: inherit; display: inline; }
html .journey-hud__route strong { color: var(--focus); font-size: inherit; }
html .journey-hud__route[data-next]::after {
content: '\00b7 ' attr(data-next) / '';
overflow: hidden;
color: #d6c3ea;
text-overflow: ellipsis;
white-space: pre;
}
html .journey-hud__arrival {
grid-column: 3;
grid-row: 2;
display: block;
color: var(--focus);
font: 720 0.56rem/1 var(--mono);
letter-spacing: 0.16em;
}
/* The read-out yields to the docked sign and returns once it has passed. */
html[data-gantry='incoming'] .journey-hud,
html[data-gantry='docked'] .journey-hud,
html[data-gantry='passing'] .journey-hud {
opacity: 0;
transform: translateY(-6px);
}
@media (max-width: 767px) {
html .journey-hud {
grid-template-rows: 1fr;
padding: 0 8px;
column-gap: 7px;
}
html .journey-hud__route,
html .journey-hud__arrival,
html .journey-hud[data-clock]::after { display: none; }
html .journey-hud[data-exit]::before { display: block; font-size: 0.56rem; }
html .journey-hud__section { font-size: 0.82rem; }
}
@media (max-width: 359px) {
html .journey-hud { display: none; }
}
/* ---------- Drive controls in the right bay ---------- */
html .drive-controls {
position: fixed;
z-index: var(--z-popover);
inset: calc((var(--gantry-h) - 44px) / 2) max(12px, env(safe-area-inset-right)) auto auto;
margin: 0;
inline-size: auto;
max-inline-size: none;
gap: 4px;
}
html .drive-controls__dock {
padding: 0;
gap: 2px;
border: 0;
background: none;
box-shadow: none;
}
html .drive-control-button,
html .drive-controls__settings-trigger {
border: 1px solid rgb(214 140 222 / 26%);
border-radius: 4px;
background: rgb(24 14 40 / 92%);
box-shadow: none;
color: #d9c9e6;
}
html .drive-control-button:hover {
border-color: rgb(255 160 220 / 55%);
background: #2f1c45;
color: #ffd1ec;
}
html .drive-start {
min-inline-size: 0;
gap: 10px;
padding-inline: 12px 14px;
border-radius: 4px;
font-size: 0.6rem;
letter-spacing: 0.12em;
}
@media (max-width: 1279px) {
html .drive-start { inline-size: 44px; padding: 0; justify-content: center; }
html .drive-start > span { display: none; }
}
/* Tooltips and popovers open downward from the beam. */
html .drive-control-button::after {
inset: calc(100% + 0.5rem) auto auto 50%;
transform: translate(-50%, -4px);
}
html .drive-control-button:hover::after,
html .drive-control-button:focus-visible::after {
transform: translate(-50%, 0);
}
html .drive-popover {
position: absolute;
inset: calc(100% + 0.7rem) 0 auto auto;
max-inline-size: calc(100vw - 1rem);
transform-origin: right top;
}
html .drive-controls__audio-status {
position: absolute;
inset: calc(100% + 0.6rem) 0 auto auto;
inline-size: max-content;
max-inline-size: calc(100vw - 1.5rem);
margin: 0;
padding: 0.55rem 0.75rem;
border: 1px solid rgb(214 140 222 / 32%);
border-radius: 4px;
}
@media (max-height: 420px) {
html .drive-controls { position: fixed; inset: calc((var(--gantry-h) - 44px) / 2) 12px auto auto; margin: 0; }
html .journey-hud { display: grid; }
html .drive-popover { inset: calc(100% + 0.5rem) 0 auto auto; }
}
/* ---------- Compact screens ---------- */
@media (max-width: 767px) {
html .site-navigation__disclosure {
position: static;
block-size: 100%;
}
html .site-navigation summary {
inline-size: var(--bay-l);
min-block-size: var(--gantry-h);
padding-inline: 16px 10px;
font-size: 0.7rem;
}
/* The window names the exit; the summary keeps its open/close sign. */
html .site-navigation summary[data-exit]::after {
content: '+' / '';
margin: 0;
padding: 0;
background: none;
color: var(--focus);
font: 500 1.1rem/1 var(--mono);
letter-spacing: 0;
}
html .site-navigation details[open] summary[data-exit]::after { content: '\2212' / ''; }
html .site-navigation ul {
inset: var(--gantry-h) 0 auto;
background: linear-gradient(180deg, var(--gantry-steel-1), var(--gantry-steel-2));
}
}
/* ---------- Content lands right under the beam ---------- */
html[data-expressway='live'] .portfolio-section,
html[data-expressway='live'] #contact,
html[data-expressway='live'] .portfolio-section[data-layout='project-stage'] {
--chapter-landing-space: calc(var(--sign-reading-top, 90px) + 10px);
--chapter-reading-inset: 10px;
}
/* ---------- Preferences ---------- */
@media (prefers-reduced-motion: reduce) {
.gantry-window,
.gantry-window::before,
.gantry-window::after,
html .journey-hud,
html .site-navigation li::after { transition: none; }
}
@media (forced-colors: active) {
html .site-navigation::before { background: Canvas; }
html .site-navigation::after { color: CanvasText; }
.gantry-window { border: 1px solid ButtonText; box-shadow: none; }
.gantry-window::before,
.gantry-window::after { border-color: Highlight; }
html .journey-hud { background: Canvas; color: CanvasText; border: 1px solid CanvasText; }
html .site-navigation li::after { background: CanvasText; box-shadow: none; }
html .site-navigation li[data-route='here']::after { background: Highlight; }
}
@media print {
.gantry-window { display: none !important; }
}