feat: make the edge-lit LCD the site's nav bar
nav-bar.css (after gantry.css) draws the bargraph, caret, indicator, chapter drop and ripple, route glints, brake, self-test and shimmer in LCD pixels under gantry's mask, and masks the wave to the lower bezel. Rules hang off html:root, the same weight the preview attribute had, so they still beat gantry.css. useNavBarSignals mirrors the speedometer, drive controls and chapter onto the bar, writing only on change; the per-frame speed pair goes on the LCD and wave rather than the nav, which keeps the per-frame restyle small. SiteNavigation renders just the parts the bar uses.
This commit is contained in:
@@ -13,6 +13,7 @@ import './styles/title-navigation.css';
|
|||||||
import './styles/wayfinding.css';
|
import './styles/wayfinding.css';
|
||||||
import './styles/route.css';
|
import './styles/route.css';
|
||||||
import './styles/gantry.css';
|
import './styles/gantry.css';
|
||||||
|
import './styles/nav-bar.css';
|
||||||
import './styles/cards.css';
|
import './styles/cards.css';
|
||||||
import './styles/maintenance.css';
|
import './styles/maintenance.css';
|
||||||
import './styles/print.css';
|
import './styles/print.css';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { BeamSample } from "@/journey/headlight-beam";
|
|||||||
import type { JourneySectionsBinding } from "@/composables/useJourneySections";
|
import type { JourneySectionsBinding } from "@/composables/useJourneySections";
|
||||||
import { useSpeedometer } from "@/composables/useSpeedometer";
|
import { useSpeedometer } from "@/composables/useSpeedometer";
|
||||||
import { SIGN_ACCENTS } from "@/journey/sign-routes";
|
import { SIGN_ACCENTS } from "@/journey/sign-routes";
|
||||||
|
import { useNavBarSignals } from "@/composables/useNavBarSignals";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
items: ReadonlyArray<{ id: SectionId; label: string }>;
|
items: ReadonlyArray<{ id: SectionId; label: string }>;
|
||||||
@@ -25,6 +26,14 @@ const props = defineProps<{
|
|||||||
animated?: boolean;
|
animated?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
/** The bar's LCD layers (styles/nav-bar.css), in paint order: layers on the
|
||||||
|
* same z-index stack in this order. */
|
||||||
|
const LCD_PARTS = [
|
||||||
|
"fill-l", "fill-r", "unlit-l", "unlit-r", "ghost",
|
||||||
|
"glint", "glint-2", "fibre-glint", "fibre-glint-2",
|
||||||
|
"caret", "indicator", "test", "brake", "shimmer", "drop",
|
||||||
|
] as const;
|
||||||
|
|
||||||
const navigationLabels: Readonly<Record<SectionId, string>> = {
|
const navigationLabels: Readonly<Record<SectionId, string>> = {
|
||||||
start: "Start",
|
start: "Start",
|
||||||
profile: "Profile",
|
profile: "Profile",
|
||||||
@@ -159,6 +168,7 @@ const speedometer = useSpeedometer({
|
|||||||
return `${route} \u00b7 ${exit} \u00b7 ${ahead}`;
|
return `${route} \u00b7 ${exit} \u00b7 ${ahead}`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
useNavBarSignals(speedo, activeIndex);
|
||||||
const speedoLabel = computed(() => (speedometer.units.value === "mph"
|
const speedoLabel = computed(() => (speedometer.units.value === "mph"
|
||||||
? "Speedometer in miles per hour. Switch to kilometres per hour"
|
? "Speedometer in miles per hour. Switch to kilometres per hour"
|
||||||
: "Speedometer in kilometres per hour. Switch to miles per hour"));
|
: "Speedometer in kilometres per hour. Switch to miles per hour"));
|
||||||
@@ -172,6 +182,8 @@ const navigate = (event: MouseEvent): void => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="site-navigation" aria-label="Portfolio sections" :data-enhanced="enhanced" :data-docked="docked" :data-motion="allowMotion === true" :style="{ '--nav-rest-scale': restScale, '--route-index': activeIndex, '--route-count': items.length, '--sign-accent': SIGN_ACCENTS[items[activeIndex]?.id ?? 'start'] }">
|
<nav class="site-navigation" aria-label="Portfolio sections" :data-enhanced="enhanced" :data-docked="docked" :data-motion="allowMotion === true" :style="{ '--nav-rest-scale': restScale, '--route-index': activeIndex, '--route-count': items.length, '--sign-accent': SIGN_ACCENTS[items[activeIndex]?.id ?? 'start'] }">
|
||||||
|
<span class="nav-lcd" aria-hidden="true"><i v-for="part in LCD_PARTS" :key="part" :class="`nav-lcd__${part}`"></i></span>
|
||||||
|
<span class="nav-wave" aria-hidden="true"></span>
|
||||||
<button
|
<button
|
||||||
ref="speedo"
|
ref="speedo"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
208
src/composables/useNavBarSignals.ts
Normal file
208
src/composables/useNavBarSignals.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* THE NAV BAR'S SIGNALS. The bar's lights (styles/nav-bar.css) and its wave
|
||||||
|
* (journey/nav-wave.ts) read the drive from custom properties and
|
||||||
|
* attributes on .site-navigation, so a static preview page can fake every
|
||||||
|
* one of them. This module only copies data the app already computes; it
|
||||||
|
* adds no frame loop:
|
||||||
|
*
|
||||||
|
* --nav-speed, --nav-throttle 0..1, mirrored from the speedometer's own
|
||||||
|
* --speed/--throttle writes (>= 1280 px).
|
||||||
|
* These change every frame while driving,
|
||||||
|
* so they go on .nav-lcd and .nav-wave,
|
||||||
|
* their only readers: on the nav they would
|
||||||
|
* restyle everything in it each frame
|
||||||
|
* (about 2 ms, against 0.7 ms on the LCD)
|
||||||
|
* data-nav-engine off | on | parked (the speedometer's)
|
||||||
|
* data-nav-brake, data-nav-jump present while braking / on an explicit drive
|
||||||
|
* --nav-here-x, --nav-next-x, --nav-from-x
|
||||||
|
* centre of the current, next and previous
|
||||||
|
* exit, in nav px
|
||||||
|
* data-nav-shift next | prev for SHIFT_MS after a chapter change
|
||||||
|
* data-nav-audio present while the soundtrack plays unmuted
|
||||||
|
* data-nav-hue 1 | 2 | 3: how fast the wave's hue cycle
|
||||||
|
* runs, from --nav-speed with hysteresis (on
|
||||||
|
* at 0.80 / 0.88 / 0.95, off below 0.76 /
|
||||||
|
* 0.85 / 0.92). It climbs at once and steps
|
||||||
|
* down one tier per 0.7 s; then 0 for 4 s
|
||||||
|
* while the colour settles back; absent
|
||||||
|
* otherwise
|
||||||
|
*
|
||||||
|
* Once the bar is mounted it also loads the wave (a lazy chunk). Reduced
|
||||||
|
* motion and forced colours are the stylesheet's and the wave's to handle:
|
||||||
|
* the signals still describe the drive, and the layers that read them
|
||||||
|
* either stand still or are not drawn.
|
||||||
|
*/
|
||||||
|
import { onBeforeUnmount, onMounted, watch, type Ref } from 'vue';
|
||||||
|
import type { NavWave } from '@/journey/nav-wave';
|
||||||
|
|
||||||
|
/** Long enough for the longest one-shot (the drop's ghost, 1.5 s). */
|
||||||
|
const SHIFT_MS = 1600;
|
||||||
|
const VARS = ['--nav-here-x', '--nav-next-x', '--nav-from-x'];
|
||||||
|
const LIVE_VARS = ['--nav-speed', '--nav-throttle'];
|
||||||
|
const ATTRS = ['data-nav-engine', 'data-nav-brake', 'data-nav-jump', 'data-nav-shift', 'data-nav-audio', 'data-nav-hue'];
|
||||||
|
/** The hue cycle: the speed each tier comes on at, and drops out below. */
|
||||||
|
const HUE_ON = [0.8, 0.88, 0.95];
|
||||||
|
const HUE_OFF = [0.76, 0.85, 0.92];
|
||||||
|
/** The least time on a tier before the next step down, and how long '0'
|
||||||
|
* (the colour settling back) lasts. */
|
||||||
|
const HUE_STEP_MS = 700;
|
||||||
|
const HUE_SETTLE_MS = 4000;
|
||||||
|
|
||||||
|
/** The hue tier (0..3) for a speed, given the current one: hysteresis, so
|
||||||
|
* a needle hovering on a threshold does not flicker the tier. */
|
||||||
|
export function hueTier(tier: number, speed: number): number {
|
||||||
|
let next = tier;
|
||||||
|
while (next < 3 && speed >= HUE_ON[next]!) next += 1;
|
||||||
|
while (next > 0 && speed < HUE_OFF[next - 1]!) next -= 1;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNavBarSignals(anchor: Ref<HTMLElement | null>, activeIndex: Ref<number>): void {
|
||||||
|
let nav: HTMLElement | null = null;
|
||||||
|
/** Where the per-frame pair goes: the bar's LCD and wave. */
|
||||||
|
let live: HTMLElement[] = [];
|
||||||
|
let tier = 0;
|
||||||
|
let speedNow = 0;
|
||||||
|
let tierAt = 0;
|
||||||
|
let hueTimer = 0;
|
||||||
|
let shiftTimer = 0;
|
||||||
|
let shiftFrame = 0;
|
||||||
|
let wave: NavWave | null = null;
|
||||||
|
let disposed = false;
|
||||||
|
/** Last value written per property or attribute (null: absent), so the
|
||||||
|
* speedometer's per-frame writes only touch the nav when a value changes. */
|
||||||
|
const written = new Map<string, string | null>();
|
||||||
|
let speedObserver: MutationObserver | null = null;
|
||||||
|
let controlsObserver: MutationObserver | null = null;
|
||||||
|
let resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
const set = (name: string, value: string): void => {
|
||||||
|
if (written.get(name) === value) return;
|
||||||
|
written.set(name, value);
|
||||||
|
nav?.style.setProperty(name, value);
|
||||||
|
};
|
||||||
|
const setLive = (name: string, value: string): void => {
|
||||||
|
if (written.get(name) === value) return;
|
||||||
|
written.set(name, value);
|
||||||
|
for (const element of live) element.style.setProperty(name, value);
|
||||||
|
};
|
||||||
|
const attr = (name: string, value: string | null): void => {
|
||||||
|
if (written.get(name) === value) return;
|
||||||
|
written.set(name, value);
|
||||||
|
if (value === null) nav?.removeAttribute(name);
|
||||||
|
else nav?.setAttribute(name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const measure = (from: number): void => {
|
||||||
|
if (!nav) return;
|
||||||
|
const box = nav.getBoundingClientRect();
|
||||||
|
if (box.width === 0) return;
|
||||||
|
// The nav scales at rest (--nav-rest-scale): measure in its own px.
|
||||||
|
const scale = nav.offsetWidth / box.width;
|
||||||
|
const items = nav.querySelectorAll('li');
|
||||||
|
const centre = (index: number): number => {
|
||||||
|
const rect = items[Math.max(0, Math.min(items.length - 1, index))]?.getBoundingClientRect();
|
||||||
|
return rect === undefined ? 0 : (rect.left - box.left + rect.width / 2) * scale;
|
||||||
|
};
|
||||||
|
const here = centre(activeIndex.value);
|
||||||
|
set('--nav-here-x', `${here.toFixed(1)}px`);
|
||||||
|
set('--nav-next-x', `${centre(activeIndex.value + 1).toFixed(1)}px`);
|
||||||
|
set('--nav-from-x', `${(from < 0 ? here : centre(from)).toFixed(1)}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The hue tier. Up at once; down one tier per HUE_STEP_MS (a timer
|
||||||
|
// carries it on once the needle has settled and stopped writing), then
|
||||||
|
// '0' for HUE_SETTLE_MS, then the attribute goes.
|
||||||
|
const stepHue = (): void => {
|
||||||
|
window.clearTimeout(hueTimer);
|
||||||
|
const wanted = hueTier(tier, speedNow);
|
||||||
|
if (wanted === tier) return;
|
||||||
|
const now = performance.now();
|
||||||
|
if (wanted < tier && now - tierAt < HUE_STEP_MS) {
|
||||||
|
hueTimer = window.setTimeout(stepHue, tierAt + HUE_STEP_MS - now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tier = wanted > tier ? wanted : tier - 1;
|
||||||
|
tierAt = now;
|
||||||
|
attr('data-nav-hue', String(tier));
|
||||||
|
if (tier > 0) hueTimer = window.setTimeout(stepHue, HUE_STEP_MS);
|
||||||
|
else hueTimer = window.setTimeout(() => { if (tier === 0) attr('data-nav-hue', null); }, HUE_SETTLE_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mirrorSpeedo = (): void => {
|
||||||
|
const speedo = anchor.value;
|
||||||
|
if (!speedo) return;
|
||||||
|
const speed = speedo.style.getPropertyValue('--speed') || '0';
|
||||||
|
setLive('--nav-speed', speed);
|
||||||
|
speedNow = Number.parseFloat(speed) || 0;
|
||||||
|
if (hueTier(tier, speedNow) !== tier) stepHue();
|
||||||
|
setLive('--nav-throttle', speedo.style.getPropertyValue('--throttle') || '0');
|
||||||
|
attr('data-nav-engine', speedo.dataset.engine ?? 'off');
|
||||||
|
attr('data-nav-brake', speedo.hasAttribute('data-brake') ? '' : null);
|
||||||
|
attr('data-nav-jump', speedo.hasAttribute('data-jump') ? '' : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mirrorControls = (): void => {
|
||||||
|
const controls = document.querySelector<HTMLElement>('.drive-controls');
|
||||||
|
attr('data-nav-audio', controls?.dataset.driveState === 'running' && controls.dataset.muted === 'false' ? '' : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// A chapter change: drop the one-shot now, then re-measure and restart it
|
||||||
|
// in a task after the next frame has rendered. That frame renders without
|
||||||
|
// the attribute, so the CSS animations restart even when changes come
|
||||||
|
// faster than SHIFT_MS, and the measuring reads a clean layout instead of
|
||||||
|
// forcing one inside the journey's frame callback.
|
||||||
|
watch(activeIndex, (index, previous) => {
|
||||||
|
if (!nav) return;
|
||||||
|
window.clearTimeout(shiftTimer);
|
||||||
|
window.cancelAnimationFrame(shiftFrame);
|
||||||
|
attr('data-nav-shift', null);
|
||||||
|
const direction = index > previous ? 'next' : 'prev';
|
||||||
|
shiftFrame = window.requestAnimationFrame(() => {
|
||||||
|
shiftTimer = window.setTimeout(() => {
|
||||||
|
measure(previous);
|
||||||
|
attr('data-nav-shift', direction);
|
||||||
|
shiftTimer = window.setTimeout(() => attr('data-nav-shift', null), SHIFT_MS);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nav = anchor.value?.closest<HTMLElement>('.site-navigation') ?? null;
|
||||||
|
if (!nav || !anchor.value) return;
|
||||||
|
live = [...nav.querySelectorAll<HTMLElement>(':scope > .nav-lcd, :scope > .nav-wave')];
|
||||||
|
speedObserver = new MutationObserver(mirrorSpeedo);
|
||||||
|
speedObserver.observe(anchor.value, { attributes: true, attributeFilter: ['style', 'data-engine', 'data-brake', 'data-jump'] });
|
||||||
|
// The drive controls mount after the nav; watch the body for their state.
|
||||||
|
controlsObserver = new MutationObserver(mirrorControls);
|
||||||
|
controlsObserver.observe(document.body, { subtree: true, attributes: true, attributeFilter: ['data-drive-state', 'data-muted'] });
|
||||||
|
resizeObserver = new ResizeObserver(() => measure(-1));
|
||||||
|
resizeObserver.observe(nav);
|
||||||
|
mirrorSpeedo();
|
||||||
|
mirrorControls();
|
||||||
|
measure(-1);
|
||||||
|
import('@/journey/nav-wave').then((module) => {
|
||||||
|
if (!disposed && nav && !wave) wave = module.mountNavWave(nav);
|
||||||
|
}, () => { /* No wave: the band stays dark, the bar works without it. */ });
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
disposed = true;
|
||||||
|
speedObserver?.disconnect();
|
||||||
|
controlsObserver?.disconnect();
|
||||||
|
resizeObserver?.disconnect();
|
||||||
|
speedObserver = controlsObserver = null;
|
||||||
|
resizeObserver = null;
|
||||||
|
window.clearTimeout(shiftTimer);
|
||||||
|
window.cancelAnimationFrame(shiftFrame);
|
||||||
|
window.clearTimeout(hueTimer);
|
||||||
|
wave?.dispose();
|
||||||
|
wave = null;
|
||||||
|
for (const name of VARS) nav?.style.removeProperty(name);
|
||||||
|
for (const element of live) for (const name of LIVE_VARS) element.style.removeProperty(name);
|
||||||
|
live = [];
|
||||||
|
for (const name of ATTRS) nav?.removeAttribute(name);
|
||||||
|
written.clear();
|
||||||
|
nav = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
471
src/styles/nav-bar.css
Normal file
471
src/styles/nav-bar.css
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
THE NAV BAR: an edge-lit LCD.
|
||||||
|
|
||||||
|
The bar is a dashboard display lit from its top edge. A fibre runs along
|
||||||
|
the top and feeds a backlight into the plate; the dash draws its gauges
|
||||||
|
in LCD pixels in front of it:
|
||||||
|
- the bargraph (two pixel rows near the top): how hard the drive is
|
||||||
|
pulling, filling outward from both ends toward the sign's window, cool
|
||||||
|
to warm to red, with light flowing behind its pixels;
|
||||||
|
- the caret over the current exit, and an indicator by the next one
|
||||||
|
while the gantry announces it;
|
||||||
|
- a drop of light that runs from the old exit to the new one on a
|
||||||
|
change of chapter, and lands with a ripple;
|
||||||
|
- route glints drifting from the current exit to the next one;
|
||||||
|
- the brake columns at both ends, the self-test when the engine starts
|
||||||
|
and the fibre's shimmer while the soundtrack plays;
|
||||||
|
- the wave in the lower bezel (journey/nav-wave.ts, a canvas): a calm
|
||||||
|
ribbon at cruise, a spectrum at speed, with a hue cycle near the top
|
||||||
|
of the dial.
|
||||||
|
|
||||||
|
Contract:
|
||||||
|
- The drive reaches the bar as custom properties and attributes on
|
||||||
|
.site-navigation (composables/useNavBarSignals.ts), so a static page
|
||||||
|
can fake every one: --nav-speed, --nav-throttle (0..1; the app
|
||||||
|
writes these two on .nav-lcd and .nav-wave instead, to keep the
|
||||||
|
per-frame restyle small),
|
||||||
|
data-nav-engine, data-nav-brake, data-nav-jump, data-nav-shift
|
||||||
|
(next|prev), data-nav-audio, data-nav-hue, --nav-here-x / --nav-next-x
|
||||||
|
/ --nav-from-x (px), html[data-gantry], html[data-underpass],
|
||||||
|
html[data-dawn].
|
||||||
|
- The moving parts live in two decorative spans (SiteNavigation.vue):
|
||||||
|
.nav-lcd carries a copy of gantry.css's mask, so the sign window, the
|
||||||
|
8 px bulge and the sign are untouched; .nav-wave is masked to the
|
||||||
|
wave's band.
|
||||||
|
- Light moves under static masks (pixels): only transform, translate,
|
||||||
|
scale and opacity animate, and the signals change custom properties,
|
||||||
|
never layout.
|
||||||
|
- State (the bargraph's level, the caret) is drawn in every mode;
|
||||||
|
animation and transitions run only with prefers-reduced-motion:
|
||||||
|
no-preference and data-motion='true'. Forced colours: no layers.
|
||||||
|
- The label band (numerals 21-30 px, labels 36-49 px at 72 px) is kept
|
||||||
|
clear: the light lives above it (0-20 px) and in the lower bezel.
|
||||||
|
- Specificity: these rules are written against `html:root
|
||||||
|
.site-navigation`. :root always matches; it gives the bar the one
|
||||||
|
class of weight over gantry.css's `html .site-navigation` rules that
|
||||||
|
the bar is tuned to win by.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/* ---------- Palette ---------- */
|
||||||
|
html:root {
|
||||||
|
--bar-mark: 191 174 211;
|
||||||
|
--bar-base-1: #110b1d;
|
||||||
|
--bar-base-2: #0c0815;
|
||||||
|
/* The wave's hue cycle at speed: neon pink, violet, cyan, coral. All
|
||||||
|
light enough to stand off the dark slab. */
|
||||||
|
--bar-hue-1: #ff6fb1;
|
||||||
|
--bar-hue-2: #a985ff;
|
||||||
|
--bar-hue-3: #55dcff;
|
||||||
|
--bar-hue-4: #ff8c6c;
|
||||||
|
}
|
||||||
|
html:root:is([data-dawn='sunrise'], [data-dawn='day']) {
|
||||||
|
--bar-mark: 255 206 150;
|
||||||
|
--bar-base-1: #161018;
|
||||||
|
--bar-base-2: #0f0a12;
|
||||||
|
/* Warmer with the sunrise: coral, amber, rose, lilac. */
|
||||||
|
--bar-hue-1: #ff8c6c;
|
||||||
|
--bar-hue-2: #ffc670;
|
||||||
|
--bar-hue-3: #ff70a6;
|
||||||
|
--bar-hue-4: #c39bff;
|
||||||
|
}
|
||||||
|
html:root .site-navigation {
|
||||||
|
--bar-on: 1;
|
||||||
|
--bar-light: var(--sign-accent, #ff83c2);
|
||||||
|
--bar-core: color-mix(in srgb, var(--bar-light) 45%, white);
|
||||||
|
--bar-cool: color-mix(in srgb, var(--bar-light) 78%, white);
|
||||||
|
--bar-glow: color-mix(in srgb, var(--bar-light) 70%, transparent);
|
||||||
|
--bar-warm: #ffd693;
|
||||||
|
--bar-red: #ff5a6e;
|
||||||
|
/* The backlight the plate sheds behind the LCD. */
|
||||||
|
--bar-spill: 30%;
|
||||||
|
--bar-dark: color-mix(in srgb, var(--bar-base-2) 80%, transparent);
|
||||||
|
/* Each half of the bargraph reaches from its outer end to the window. */
|
||||||
|
--bar-reach: calc(50cqw - var(--window-w) / 2 - 28px);
|
||||||
|
/* The drop's row: the middle of the bargraph. */
|
||||||
|
--bar-fy: 8px;
|
||||||
|
/* The wave's band in the lower bezel. The ribbon swings about its middle;
|
||||||
|
the spectrum rises from its floor to just under the labels. */
|
||||||
|
--bar-band-top: calc(var(--gantry-h) - 20px);
|
||||||
|
--bar-band-h: 18px;
|
||||||
|
/* The wave's standard colour (the canvas resolves it, and the palette). */
|
||||||
|
--bar-wave: color-mix(in srgb, var(--bar-cool) 55%, rgb(var(--bar-mark)));
|
||||||
|
}
|
||||||
|
html:root:is([data-dawn='sunrise'], [data-dawn='day']) .site-navigation { --bar-spill: 20%; }
|
||||||
|
html:root .site-navigation:is([data-nav-engine='off'], [data-nav-engine='parked']) { --bar-on: 0; }
|
||||||
|
/* The bargraph's level, and the fast run of light behind it that shows as
|
||||||
|
the drive picks up. They are resolved on .nav-lcd, not the nav: the
|
||||||
|
speed changes every frame while driving, and a custom property changing
|
||||||
|
on the nav restyles everything in it (about 2 ms a frame), where on the
|
||||||
|
LCD it stays inside the lights (see useNavBarSignals.ts). */
|
||||||
|
html:root .site-navigation > .nav-lcd {
|
||||||
|
--bar-level: calc(var(--bar-on) * (0.08 + var(--nav-speed, 0.37) * 0.6 + var(--nav-throttle, 0) * 0.32));
|
||||||
|
--bar-fast: clamp(0, var(--nav-speed, 0.37) * 1.5 + var(--nav-throttle, 0) - 0.45, 1);
|
||||||
|
}
|
||||||
|
html:root .site-navigation[data-nav-jump] > .nav-lcd { --bar-fast: 1; }
|
||||||
|
@media (max-width: 1279px) {
|
||||||
|
html:root .site-navigation { --bar-reach: calc(50cqw - 8px); }
|
||||||
|
}
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
/* A 56 px bar: the band shrinks with it. */
|
||||||
|
html:root .site-navigation { --bar-band-top: calc(var(--gantry-h) - 16px); --bar-band-h: 14px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- The slab. The LCD's black matrix is full over the bargraph's
|
||||||
|
two pixel rows and only a faint etch below them. ---------- */
|
||||||
|
html:root .site-navigation:is(*, [data-enhanced='true'][data-docked='true'])::before {
|
||||||
|
background:
|
||||||
|
/* The fibre along the top edge, feeding the plate: a hot core, then a
|
||||||
|
prismatic fringe where it enters (warm over cool). */
|
||||||
|
linear-gradient(180deg, var(--bar-core) 0 1px, color-mix(in srgb, var(--bar-light) 55%, transparent) 1px 2px, rgb(255 120 150 / 22%) 2px 3px, rgb(110 200 255 / 18%) 3px 4px, transparent 4px) 0 0 / 100% 4px no-repeat,
|
||||||
|
/* The bargraph's rows: the full black matrix, the backlight showing
|
||||||
|
through 3 px pixels... */
|
||||||
|
repeating-linear-gradient(90deg, transparent 0 3px, rgb(4 2 10 / 62%) 3px 4px) 0 4px / 100% 8px no-repeat,
|
||||||
|
repeating-linear-gradient(180deg, transparent 0 3px, rgb(4 2 10 / 62%) 3px 4px) 0 4px / 100% 8px no-repeat,
|
||||||
|
/* ...and below them only a faint etch, over an even tint that keeps
|
||||||
|
the slab as dark as a full matrix would. */
|
||||||
|
repeating-linear-gradient(90deg, transparent 0 3px, rgb(4 2 10 / 13%) 3px 4px) 0 12px / 100% calc(var(--gantry-h) - 14px) no-repeat,
|
||||||
|
repeating-linear-gradient(180deg, transparent 0 3px, rgb(4 2 10 / 13%) 3px 4px) 0 12px / 100% calc(var(--gantry-h) - 14px) no-repeat,
|
||||||
|
linear-gradient(rgb(4 2 10 / 22%), rgb(4 2 10 / 22%)) 0 12px / 100% calc(var(--gantry-h) - 14px) no-repeat,
|
||||||
|
/* Recessed lower bezel with the light's hairline. */
|
||||||
|
linear-gradient(180deg, rgb(0 0 0 / 60%) 0 1px, color-mix(in srgb, var(--bar-light) 30%, transparent) 1px 2px) 0 calc(var(--gantry-h) - 2px) / 100% 2px no-repeat,
|
||||||
|
/* The backlight: fed at the top edge and both ends, falling off downward. */
|
||||||
|
radial-gradient(22% 140% at 0 0, color-mix(in srgb, var(--bar-light) 16%, transparent), transparent 70%) 0 0 / 100% var(--gantry-h) no-repeat,
|
||||||
|
radial-gradient(22% 140% at 100% 0, color-mix(in srgb, var(--bar-light) 16%, transparent), transparent 70%) 0 0 / 100% var(--gantry-h) no-repeat,
|
||||||
|
linear-gradient(180deg, color-mix(in srgb, var(--bar-light) var(--bar-spill), transparent) 2px, color-mix(in srgb, var(--bar-light) 9%, transparent) 16px, color-mix(in srgb, var(--bar-light) 3%, transparent) 40%, transparent 72%) 0 0 / 100% var(--gantry-h) no-repeat,
|
||||||
|
/* The unlit pixels themselves, and the panel. */
|
||||||
|
linear-gradient(rgb(var(--bar-mark) / 4%), rgb(var(--bar-mark) / 4%)),
|
||||||
|
linear-gradient(90deg, var(--bar-base-1), var(--bar-base-2) 50%, var(--bar-base-1));
|
||||||
|
box-shadow: 0 1px 0 rgb(0 0 0 / 55%);
|
||||||
|
}
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
html:root .site-navigation:is(*, [data-enhanced='true'][data-docked='true'])::before {
|
||||||
|
background: Canvas;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- The layers ---------- */
|
||||||
|
.nav-lcd,
|
||||||
|
.nav-wave { display: none; }
|
||||||
|
@media (forced-colors: none) {
|
||||||
|
html:root .site-navigation > .nav-lcd {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1; /* After the bar's ::before in tree order: over it, under the labels. */
|
||||||
|
inset: 0 0 auto;
|
||||||
|
block-size: calc(var(--gantry-h) + var(--bulge));
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
contain: strict;
|
||||||
|
container-type: inline-size;
|
||||||
|
/* Same cut-out as the beam (gantry.css): the sign window stays clear. */
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 50% 0 / calc(var(--window-w) + 56px) 100% no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 0 0 / 100% var(--gantry-h) no-repeat;
|
||||||
|
-webkit-mask-composite: xor, source-over;
|
||||||
|
mask:
|
||||||
|
linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 50% 0 / calc(var(--window-w) + 56px) calc(var(--gantry-h) + var(--bulge)) no-repeat,
|
||||||
|
linear-gradient(#000 0 0) 0 0 / 100% var(--gantry-h) no-repeat;
|
||||||
|
mask-composite: exclude, add;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > i {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > i::before,
|
||||||
|
html:root .site-navigation > .nav-lcd > i::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
/* Exit numerals at full strength, so moving light never takes them below 4.5:1. */
|
||||||
|
html:root .site-navigation a::before { opacity: 1; }
|
||||||
|
|
||||||
|
/* The bargraph: two rows of LCD pixels, tinted cool, warm, red along the
|
||||||
|
scale, filling from each outer end toward the window. The light
|
||||||
|
flowing in the plate behind them is what lights them. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__fill-l, html:root .site-navigation > .nav-lcd > .nav-lcd__fill-r {
|
||||||
|
z-index: 1;
|
||||||
|
inset: 0;
|
||||||
|
-webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; mask-composite: intersect, intersect;
|
||||||
|
background: linear-gradient(90deg, var(--bar-cool) 0, var(--bar-cool) calc(var(--bar-reach) * 0.6), var(--bar-warm) calc(var(--bar-reach) * 0.78), var(--bar-red) calc(var(--bar-reach) * 0.92));
|
||||||
|
clip-path: inset(0 50% 0 0);
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__fill-r {
|
||||||
|
background: linear-gradient(270deg, var(--bar-cool) 0, var(--bar-cool) calc(var(--bar-reach) * 0.6), var(--bar-warm) calc(var(--bar-reach) * 0.78), var(--bar-red) calc(var(--bar-reach) * 0.92));
|
||||||
|
clip-path: inset(0 0 0 50%);
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__fill-l::before, html:root .site-navigation > .nav-lcd > .nav-lcd__fill-l::after, html:root .site-navigation > .nav-lcd > .nav-lcd__fill-r::before, html:root .site-navigation > .nav-lcd > .nav-lcd__fill-r::after {
|
||||||
|
inset: 0 auto 0 -600px;
|
||||||
|
inline-size: calc(100% + 1200px);
|
||||||
|
background: rgb(255 252 246 / 85%);
|
||||||
|
-webkit-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='8' preserveAspectRatio='none'><linearGradient id='a'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.48'/><stop offset='0.025' stop-color='%23fff' stop-opacity='0.99'/><stop offset='0.064' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.123' stop-color='%23fff' stop-opacity='0.73'/><stop offset='0.169' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.197' stop-color='%23fff' stop-opacity='0.55'/><stop offset='0.253' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.309' stop-color='%23fff' stop-opacity='0.97'/><stop offset='0.370' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.419' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.469' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.496' stop-color='%23fff' stop-opacity='0.74'/><stop offset='0.531' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.563' stop-color='%23fff' stop-opacity='0.10'/><stop offset='0.588' stop-color='%23fff' stop-opacity='0.51'/><stop offset='0.627' stop-color='%23fff' stop-opacity='0.27'/><stop offset='0.653' stop-color='%23fff' stop-opacity='0.45'/><stop offset='0.687' stop-color='%23fff' stop-opacity='0.51'/><stop offset='0.729' stop-color='%23fff' stop-opacity='0.47'/><stop offset='0.765' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.800' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.845' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.887' stop-color='%23fff' stop-opacity='0.25'/><stop offset='0.936' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.989' stop-color='%23fff' stop-opacity='0.40'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.48'/></linearGradient><rect width='600' height='8' fill='url(%23a)'/></svg>") 0 0 / 600px 100% repeat-x; mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='8' preserveAspectRatio='none'><linearGradient id='a'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.48'/><stop offset='0.025' stop-color='%23fff' stop-opacity='0.99'/><stop offset='0.064' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.123' stop-color='%23fff' stop-opacity='0.73'/><stop offset='0.169' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.197' stop-color='%23fff' stop-opacity='0.55'/><stop offset='0.253' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.309' stop-color='%23fff' stop-opacity='0.97'/><stop offset='0.370' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.419' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.469' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.496' stop-color='%23fff' stop-opacity='0.74'/><stop offset='0.531' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.563' stop-color='%23fff' stop-opacity='0.10'/><stop offset='0.588' stop-color='%23fff' stop-opacity='0.51'/><stop offset='0.627' stop-color='%23fff' stop-opacity='0.27'/><stop offset='0.653' stop-color='%23fff' stop-opacity='0.45'/><stop offset='0.687' stop-color='%23fff' stop-opacity='0.51'/><stop offset='0.729' stop-color='%23fff' stop-opacity='0.47'/><stop offset='0.765' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.800' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.845' stop-color='%23fff' stop-opacity='0.76'/><stop offset='0.887' stop-color='%23fff' stop-opacity='0.25'/><stop offset='0.936' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.989' stop-color='%23fff' stop-opacity='0.40'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.48'/></linearGradient><rect width='600' height='8' fill='url(%23a)'/></svg>") 0 0 / 600px 100% repeat-x;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__fill-l::after, html:root .site-navigation > .nav-lcd > .nav-lcd__fill-r::after {
|
||||||
|
-webkit-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='440' height='8' preserveAspectRatio='none'><linearGradient id='b'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.81'/><stop offset='0.048' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.133' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.177' stop-color='%23fff' stop-opacity='0.38'/><stop offset='0.230' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.272' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.333' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.404' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.443' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.511' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.553' stop-color='%23fff' stop-opacity='0.95'/><stop offset='0.612' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.696' stop-color='%23fff' stop-opacity='0.46'/><stop offset='0.756' stop-color='%23fff' stop-opacity='0.48'/><stop offset='0.805' stop-color='%23fff' stop-opacity='0.18'/><stop offset='0.853' stop-color='%23fff' stop-opacity='0.81'/><stop offset='0.891' stop-color='%23fff' stop-opacity='0.10'/><stop offset='0.966' stop-color='%23fff' stop-opacity='0.99'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.81'/></linearGradient><rect width='440' height='8' fill='url(%23b)'/></svg>") 0 0 / 440px 100% repeat-x; mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='440' height='8' preserveAspectRatio='none'><linearGradient id='b'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.81'/><stop offset='0.048' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.133' stop-color='%23fff' stop-opacity='0.03'/><stop offset='0.177' stop-color='%23fff' stop-opacity='0.38'/><stop offset='0.230' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.272' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.333' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.404' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.443' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.511' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.553' stop-color='%23fff' stop-opacity='0.95'/><stop offset='0.612' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.696' stop-color='%23fff' stop-opacity='0.46'/><stop offset='0.756' stop-color='%23fff' stop-opacity='0.48'/><stop offset='0.805' stop-color='%23fff' stop-opacity='0.18'/><stop offset='0.853' stop-color='%23fff' stop-opacity='0.81'/><stop offset='0.891' stop-color='%23fff' stop-opacity='0.10'/><stop offset='0.966' stop-color='%23fff' stop-opacity='0.99'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.81'/></linearGradient><rect width='440' height='8' fill='url(%23b)'/></svg>") 0 0 / 440px 100% repeat-x;
|
||||||
|
opacity: calc(var(--bar-fast) * 0.8);
|
||||||
|
}
|
||||||
|
/* What covers the bargraph beyond its level (the unlit pixels), and the
|
||||||
|
peak pixel that lingers where the level last was. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-l, html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r {
|
||||||
|
z-index: 2;
|
||||||
|
inset: 0;
|
||||||
|
-webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; mask-composite: intersect, intersect;
|
||||||
|
clip-path: inset(0 50% 0 0);
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r { clip-path: inset(0 0 0 50%); }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-l::before, html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r::before {
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--bar-dark) 8px);
|
||||||
|
translate: calc(var(--bar-level) * var(--bar-reach)) 0;
|
||||||
|
--bar-dir: 1;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r::before {
|
||||||
|
background: linear-gradient(270deg, transparent, var(--bar-dark) 8px);
|
||||||
|
translate: calc(var(--bar-level) * var(--bar-reach) * -1) 0;
|
||||||
|
--bar-dir: -1;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-l::after, html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r::after {
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
inline-size: 4px;
|
||||||
|
background: rgb(255 250 240 / 95%);
|
||||||
|
translate: calc(var(--bar-level) * var(--bar-reach) - 8px) 0;
|
||||||
|
opacity: var(--bar-on);
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__unlit-r::after { inset: 0 0 0 auto; translate: calc((var(--bar-level) * var(--bar-reach) - 8px) * -1) 0; }
|
||||||
|
/* A chapter change: a drop of light runs through the bargraph from the
|
||||||
|
old exit to the new one, stretching as it goes and squashing as it
|
||||||
|
lands, and a ripple spreads where it lands. The pixels it ran through
|
||||||
|
hold a ghost of it for a moment. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__ghost { z-index: 3; inset: 0; -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 16px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 16px no-repeat; mask-composite: intersect, intersect; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__ghost::after {
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
inline-size: 120px;
|
||||||
|
block-size: 24px;
|
||||||
|
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bar-cool) 75%, transparent));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__drop { z-index: 4; inset: 0; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__drop::before {
|
||||||
|
inset: calc(var(--bar-fy) - 5px) auto auto 0;
|
||||||
|
inline-size: 40px;
|
||||||
|
block-size: 10px;
|
||||||
|
background: radial-gradient(closest-side, #fff 30%, var(--bar-glow) 60%, transparent);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__drop::after {
|
||||||
|
inset: calc(var(--bar-fy) - 12px) auto auto 0;
|
||||||
|
inline-size: 56px;
|
||||||
|
block-size: 24px;
|
||||||
|
translate: calc(var(--nav-here-x, 0px) - 28px) 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(closest-side, transparent 52%, var(--bar-glow) 70%, transparent);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
/* The plate's own light: a short glint launched from the current exit
|
||||||
|
drifts slowly to the next one and fades there. One at rest, a second
|
||||||
|
from 42 % of the dial; through the LCD it barely lifts the pixels, in
|
||||||
|
the fibre it is a faint run. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__glint, html:root .site-navigation > .nav-lcd > .nav-lcd__glint-2 { z-index: 3; inset: 0; -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 16px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 16px no-repeat; mask-composite: intersect, intersect; opacity: 0.5; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint, html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint-2 { z-index: 3; inset: 0 0 auto; block-size: 4px; opacity: 0.5; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__glint-2, html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint-2 { opacity: calc(0.5 * clamp(0, (var(--nav-speed, 0.37) - 0.42) * 4, 1)); }
|
||||||
|
html:root .site-navigation[data-nav-jump] > .nav-lcd > :is(.nav-lcd__glint-2, .nav-lcd__fibre-glint-2) { opacity: 0.5; }
|
||||||
|
:is(html:root .site-navigation > .nav-lcd > .nav-lcd__glint, html:root .site-navigation > .nav-lcd > .nav-lcd__glint-2)::before {
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
inline-size: 28px;
|
||||||
|
block-size: 20px;
|
||||||
|
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bar-cool) 40%, transparent) 55%, rgb(255 255 255 / 45%) 85%, transparent);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
:is(html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint, html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint-2)::before {
|
||||||
|
inset: 0 auto auto 0;
|
||||||
|
inline-size: 28px;
|
||||||
|
block-size: 3px;
|
||||||
|
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--bar-light) 70%, transparent) 60%, rgb(255 255 255 / 70%) 88%, transparent);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
/* The caret over the current exit, and the indicator by the next one
|
||||||
|
while the gantry announces it. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__caret { z-index: 3; inset: 0; -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 12px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 12px / 100% 8px no-repeat; mask-composite: intersect, intersect; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__caret::before {
|
||||||
|
inset: 12px auto auto 0;
|
||||||
|
inline-size: 14px;
|
||||||
|
block-size: 8px;
|
||||||
|
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
||||||
|
background: var(--bar-core);
|
||||||
|
translate: calc(var(--nav-here-x, -40px) - 7px) 0;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__indicator { z-index: 3; inset: 0; -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 12px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 12px / 100% 8px no-repeat; mask-composite: intersect, intersect; opacity: 0; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__indicator::before {
|
||||||
|
inset: 12px auto auto 0;
|
||||||
|
inline-size: 20px;
|
||||||
|
block-size: 8px;
|
||||||
|
clip-path: polygon(0 0, 20% 0, 50% 50%, 20% 100%, 0 100%, 30% 50%, 0 0, 50% 0, 50% 0, 70% 0, 100% 50%, 70% 100%, 50% 100%, 80% 50%, 50% 0);
|
||||||
|
background: #74f0aa;
|
||||||
|
translate: calc(var(--nav-next-x, -60px) + 8px) 0;
|
||||||
|
}
|
||||||
|
html:root[data-gantry='incoming'] .site-navigation > .nav-lcd > .nav-lcd__indicator { opacity: 1; }
|
||||||
|
/* The self-test (all pixels on) as the engine starts, and the brake
|
||||||
|
columns at both ends. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__test { z-index: 3; inset: 0; -webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; mask-composite: intersect, intersect; background: color-mix(in srgb, var(--bar-cool) 80%, white); opacity: 0; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__brake {
|
||||||
|
z-index: 3;
|
||||||
|
inset: 0;
|
||||||
|
-webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 4px / 100% 8px no-repeat; mask-composite: intersect, intersect;
|
||||||
|
background: linear-gradient(90deg, var(--bar-red) 0 12px, transparent 12px calc(100% - 12px), var(--bar-red) calc(100% - 12px));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
html:root .site-navigation[data-nav-brake] > .nav-lcd > .nav-lcd__brake { opacity: 1; }
|
||||||
|
/* The fibre's shimmer, brighter while the soundtrack plays. */
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__shimmer { z-index: 3; inset: 0 0 auto; block-size: 2px; opacity: 0.45; }
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__shimmer::before, html:root .site-navigation > .nav-lcd > .nav-lcd__shimmer::after {
|
||||||
|
inset: 0 auto 0 0;
|
||||||
|
inline-size: calc(100% + 600px);
|
||||||
|
background: #fff;
|
||||||
|
-webkit-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='8' preserveAspectRatio='none'><linearGradient id='c'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.029' stop-color='%23fff' stop-opacity='0.74'/><stop offset='0.071' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.101' stop-color='%23fff' stop-opacity='0.92'/><stop offset='0.146' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.197' stop-color='%23fff' stop-opacity='0.02'/><stop offset='0.242' stop-color='%23fff' stop-opacity='0.47'/><stop offset='0.298' stop-color='%23fff' stop-opacity='0.95'/><stop offset='0.342' stop-color='%23fff' stop-opacity='0.89'/><stop offset='0.379' stop-color='%23fff' stop-opacity='0.75'/><stop offset='0.408' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.435' stop-color='%23fff' stop-opacity='0.09'/><stop offset='0.460' stop-color='%23fff' stop-opacity='0.93'/><stop offset='0.519' stop-color='%23fff' stop-opacity='0.04'/><stop offset='0.548' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.573' stop-color='%23fff' stop-opacity='0.90'/><stop offset='0.613' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.666' stop-color='%23fff' stop-opacity='0.24'/><stop offset='0.709' stop-color='%23fff' stop-opacity='0.99'/><stop offset='0.745' stop-color='%23fff' stop-opacity='0.38'/><stop offset='0.782' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.842' stop-color='%23fff' stop-opacity='0.05'/><stop offset='0.866' stop-color='%23fff' stop-opacity='0.34'/><stop offset='0.893' stop-color='%23fff' stop-opacity='0.26'/><stop offset='0.937' stop-color='%23fff' stop-opacity='0.42'/><stop offset='0.981' stop-color='%23fff' stop-opacity='0.08'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.11'/></linearGradient><rect width='600' height='8' fill='url(%23c)'/></svg>") 0 0 / 600px 100% repeat-x; mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='600' height='8' preserveAspectRatio='none'><linearGradient id='c'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.029' stop-color='%23fff' stop-opacity='0.74'/><stop offset='0.071' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.101' stop-color='%23fff' stop-opacity='0.92'/><stop offset='0.146' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.197' stop-color='%23fff' stop-opacity='0.02'/><stop offset='0.242' stop-color='%23fff' stop-opacity='0.47'/><stop offset='0.298' stop-color='%23fff' stop-opacity='0.95'/><stop offset='0.342' stop-color='%23fff' stop-opacity='0.89'/><stop offset='0.379' stop-color='%23fff' stop-opacity='0.75'/><stop offset='0.408' stop-color='%23fff' stop-opacity='0.01'/><stop offset='0.435' stop-color='%23fff' stop-opacity='0.09'/><stop offset='0.460' stop-color='%23fff' stop-opacity='0.93'/><stop offset='0.519' stop-color='%23fff' stop-opacity='0.04'/><stop offset='0.548' stop-color='%23fff' stop-opacity='0.08'/><stop offset='0.573' stop-color='%23fff' stop-opacity='0.90'/><stop offset='0.613' stop-color='%23fff' stop-opacity='0.77'/><stop offset='0.666' stop-color='%23fff' stop-opacity='0.24'/><stop offset='0.709' stop-color='%23fff' stop-opacity='0.99'/><stop offset='0.745' stop-color='%23fff' stop-opacity='0.38'/><stop offset='0.782' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.842' stop-color='%23fff' stop-opacity='0.05'/><stop offset='0.866' stop-color='%23fff' stop-opacity='0.34'/><stop offset='0.893' stop-color='%23fff' stop-opacity='0.26'/><stop offset='0.937' stop-color='%23fff' stop-opacity='0.42'/><stop offset='0.981' stop-color='%23fff' stop-opacity='0.08'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.11'/></linearGradient><rect width='600' height='8' fill='url(%23c)'/></svg>") 0 0 / 600px 100% repeat-x;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__shimmer::after { inset: 0 auto 0 -437px; inline-size: calc(100% + 437px); -webkit-mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='437' height='8' preserveAspectRatio='none'><linearGradient id='d'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.00'/><stop offset='0.059' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.096' stop-color='%23fff' stop-opacity='0.96'/><stop offset='0.153' stop-color='%23fff' stop-opacity='0.24'/><stop offset='0.191' stop-color='%23fff' stop-opacity='0.50'/><stop offset='0.242' stop-color='%23fff' stop-opacity='0.88'/><stop offset='0.290' stop-color='%23fff' stop-opacity='0.72'/><stop offset='0.351' stop-color='%23fff' stop-opacity='0.41'/><stop offset='0.388' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.444' stop-color='%23fff' stop-opacity='0.35'/><stop offset='0.516' stop-color='%23fff' stop-opacity='0.07'/><stop offset='0.559' stop-color='%23fff' stop-opacity='0.00'/><stop offset='0.630' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.664' stop-color='%23fff' stop-opacity='0.45'/><stop offset='0.702' stop-color='%23fff' stop-opacity='0.09'/><stop offset='0.743' stop-color='%23fff' stop-opacity='0.98'/><stop offset='0.779' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.827' stop-color='%23fff' stop-opacity='0.02'/><stop offset='0.871' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.924' stop-color='%23fff' stop-opacity='0.26'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.00'/></linearGradient><rect width='437' height='8' fill='url(%23d)'/></svg>") 0 0 / 437px 100% repeat-x; mask: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='437' height='8' preserveAspectRatio='none'><linearGradient id='d'><stop offset='0.000' stop-color='%23fff' stop-opacity='0.00'/><stop offset='0.059' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.096' stop-color='%23fff' stop-opacity='0.96'/><stop offset='0.153' stop-color='%23fff' stop-opacity='0.24'/><stop offset='0.191' stop-color='%23fff' stop-opacity='0.50'/><stop offset='0.242' stop-color='%23fff' stop-opacity='0.88'/><stop offset='0.290' stop-color='%23fff' stop-opacity='0.72'/><stop offset='0.351' stop-color='%23fff' stop-opacity='0.41'/><stop offset='0.388' stop-color='%23fff' stop-opacity='0.11'/><stop offset='0.444' stop-color='%23fff' stop-opacity='0.35'/><stop offset='0.516' stop-color='%23fff' stop-opacity='0.07'/><stop offset='0.559' stop-color='%23fff' stop-opacity='0.00'/><stop offset='0.630' stop-color='%23fff' stop-opacity='0.06'/><stop offset='0.664' stop-color='%23fff' stop-opacity='0.45'/><stop offset='0.702' stop-color='%23fff' stop-opacity='0.09'/><stop offset='0.743' stop-color='%23fff' stop-opacity='0.98'/><stop offset='0.779' stop-color='%23fff' stop-opacity='0.94'/><stop offset='0.827' stop-color='%23fff' stop-opacity='0.02'/><stop offset='0.871' stop-color='%23fff' stop-opacity='0.71'/><stop offset='0.924' stop-color='%23fff' stop-opacity='0.26'/><stop offset='1.000' stop-color='%23fff' stop-opacity='0.00'/></linearGradient><rect width='437' height='8' fill='url(%23d)'/></svg>") 0 0 / 437px 100% repeat-x; }
|
||||||
|
html:root .site-navigation[data-nav-audio] > .nav-lcd > .nav-lcd__shimmer { opacity: 1; }
|
||||||
|
|
||||||
|
/* The wave's band, in LCD pixels along the lower bezel. The canvas
|
||||||
|
(journey/nav-wave.ts) draws the line; the mask stands still, so the
|
||||||
|
line moves under the pixels. */
|
||||||
|
html:root .site-navigation > .nav-wave {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
inset: 0 0 auto;
|
||||||
|
block-size: calc(var(--gantry-h) + var(--bulge));
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
contain: strict;
|
||||||
|
-webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 var(--bar-band-top) / 100% var(--bar-band-h) no-repeat, linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat, linear-gradient(#000 0 0) 0 0 / 100% var(--gantry-h) no-repeat;
|
||||||
|
-webkit-mask-composite: source-in, source-in, source-in, xor;
|
||||||
|
mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 var(--bar-band-top) / 100% var(--bar-band-h) no-repeat, linear-gradient(#000 0 0) var(--window-mask-x) var(--window-y) / var(--window-w) var(--window-h) no-repeat, linear-gradient(#000 0 0) 0 0 / 100% var(--gantry-h) no-repeat;
|
||||||
|
mask-composite: intersect, intersect, intersect, exclude;
|
||||||
|
}
|
||||||
|
html:root .site-navigation > .nav-wave > canvas {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: var(--bar-band-top) 0 auto 0;
|
||||||
|
inline-size: 100%;
|
||||||
|
block-size: var(--bar-band-h);
|
||||||
|
}
|
||||||
|
/* Under 768 px the exits fold into the menu: no caret, no indicator, and
|
||||||
|
nothing to travel between, so no drop and no glints (they would only
|
||||||
|
pulse at the bar's left edge). */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
html:root .site-navigation > .nav-lcd > .nav-lcd__caret, html:root .site-navigation > .nav-lcd > .nav-lcd__indicator, html:root .site-navigation > .nav-lcd > .nav-lcd__drop, html:root .site-navigation > .nav-lcd > .nav-lcd__glint, html:root .site-navigation > .nav-lcd > .nav-lcd__glint-2, html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint, html:root .site-navigation > .nav-lcd > .nav-lcd__fibre-glint-2 { display: none; }
|
||||||
|
}
|
||||||
|
/* A pixel caret over the exit under the pointer or focus. */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html:root .site-navigation li::before {
|
||||||
|
content: '' / '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 12px auto auto calc(50% - 6px);
|
||||||
|
inline-size: 12px;
|
||||||
|
block-size: 8px;
|
||||||
|
background: linear-gradient(rgb(var(--bar-mark) / 85%), rgb(var(--bar-mark) / 85%));
|
||||||
|
-webkit-mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 0 / 100% 100% no-repeat; -webkit-mask-composite: source-in, source-in; mask: repeating-linear-gradient(90deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, repeating-linear-gradient(180deg, #000 0 3px, transparent 3px 4px) 0 0 / 100% 100%, linear-gradient(#000 0 0) 0 0 / 100% 100% no-repeat; mask-composite: intersect, intersect;
|
||||||
|
clip-path: polygon(0 0, 100% 0, 50% 100%);
|
||||||
|
opacity: 0;
|
||||||
|
translate: 0 -4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
html:root .site-navigation li:has(a:is(:hover, :focus-visible)):not([data-route='here'])::before { opacity: 1; translate: 0 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Motion ---------- */
|
||||||
|
@media (prefers-reduced-motion: no-preference) and (forced-colors: none) {
|
||||||
|
/* The light flowing behind the bargraph: outward on the left, inward on
|
||||||
|
the right; the fast run fades in with the speed. */
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fill-l::before { animation: bar-flow-in 4.6s linear infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fill-l::after { animation: bar-flow-in-fast 1.25s linear infinite; transition: opacity 500ms ease; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fill-r::before { animation: bar-flow-out 4.6s linear -2.3s infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fill-r::after { animation: bar-flow-out-fast 1.25s linear -0.6s infinite; transition: opacity 500ms ease; }
|
||||||
|
/* The level eases to the speed and hunts a little about it; with the
|
||||||
|
engine off it falls away in steps, and the peak pixel follows late. */
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__unlit-l::before, html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__unlit-r::before {
|
||||||
|
transition: translate 320ms cubic-bezier(0.3, 0.7, 0.4, 1);
|
||||||
|
animation: bar-hunt 6.7s steps(1, end) infinite;
|
||||||
|
}
|
||||||
|
html:root .site-navigation[data-motion='true']:is([data-nav-engine='off'], [data-nav-engine='parked']) > .nav-lcd > :is(.nav-lcd__unlit-l, .nav-lcd__unlit-r)::before { transition-duration: 1.6s; transition-timing-function: steps(10, end); }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__unlit-l::after, html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__unlit-r::after { transition: translate 1.6s steps(12, end) 500ms, opacity 600ms ease; }
|
||||||
|
html:root .site-navigation[data-motion='true'][data-nav-engine='on'] > .nav-lcd > .nav-lcd__test { animation: bar-self-test 1.5s linear both; }
|
||||||
|
/* The drop, its ripple, and the ghost in the pixels behind it. */
|
||||||
|
html:root .site-navigation[data-motion='true'][data-nav-shift] > .nav-lcd > .nav-lcd__drop::before { animation: bar-drop 900ms both; }
|
||||||
|
html:root .site-navigation[data-motion='true'][data-nav-shift] > .nav-lcd > .nav-lcd__drop::after { animation: bar-ripple 900ms cubic-bezier(0.2, 0.7, 0.3, 1) 600ms both; }
|
||||||
|
html:root .site-navigation[data-motion='true'][data-nav-shift] > .nav-lcd > .nav-lcd__ghost::after { animation: bar-ghost 1500ms both; }
|
||||||
|
/* The route glints, slow; they stop at the next exit. */
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__glint::before, html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fibre-glint::before { animation: bar-glint 5.8s cubic-bezier(0.45, 0.05, 0.35, 1) infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__glint-2::before, html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fibre-glint-2::before { animation: bar-glint 5.8s cubic-bezier(0.45, 0.05, 0.35, 1) -2.9s infinite; }
|
||||||
|
:is(html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__glint-2, html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__fibre-glint-2) { transition: opacity 900ms ease; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__caret::before { transition: translate 820ms cubic-bezier(0.34, 1.3, 0.55, 1) 420ms; }
|
||||||
|
html:root[data-gantry='incoming'] .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__indicator { animation: bar-blink 760ms steps(1, end) infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__brake { transition: opacity 600ms steps(3, end); }
|
||||||
|
html:root .site-navigation[data-motion='true'][data-nav-brake] > .nav-lcd > .nav-lcd__brake { transition: opacity 100ms; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__shimmer { transition: opacity 900ms ease; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__shimmer::before { animation: bar-shimmer 19.3s linear infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > .nav-lcd > .nav-lcd__shimmer::after { animation: bar-shimmer-back 27.1s linear infinite; }
|
||||||
|
html:root .site-navigation[data-motion='true'] > :is(.nav-lcd, .nav-wave) { transition: opacity 900ms ease; }
|
||||||
|
html:root[data-underpass='under'] .site-navigation > :is(.nav-lcd, .nav-wave) { opacity: 0.68; }
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html:root .site-navigation[data-motion='true'] li::before { transition: opacity 160ms steps(2, end), translate 360ms steps(2, end); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bar-flow-in { from { transform: translateX(0); } to { transform: translateX(600px); } }
|
||||||
|
@keyframes bar-flow-in-fast { from { transform: translateX(0); } to { transform: translateX(440px); } }
|
||||||
|
@keyframes bar-flow-out { from { transform: translateX(0); } to { transform: translateX(-600px); } }
|
||||||
|
@keyframes bar-flow-out-fast { from { transform: translateX(0); } to { transform: translateX(-440px); } }
|
||||||
|
@keyframes bar-hunt {
|
||||||
|
0%, 22%, 50%, 70%, 100% { transform: none; }
|
||||||
|
6% { transform: translateX(calc(var(--bar-dir) * 4px)); }
|
||||||
|
12% { transform: translateX(calc(var(--bar-dir) * -4px)); }
|
||||||
|
38% { transform: translateX(calc(var(--bar-dir) * 12px)); }
|
||||||
|
42% { transform: translateX(calc(var(--bar-dir) * 4px)); }
|
||||||
|
80% { transform: translateX(calc(var(--bar-dir) * 4px)); }
|
||||||
|
}
|
||||||
|
/* One pulse, 1.5 s: on at 8 %, off from 55 % to 62 %. */
|
||||||
|
@keyframes bar-self-test {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
8%, 55% { opacity: 1; }
|
||||||
|
62%, 100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
/* About 1.3 Hz, far under the 3 Hz flash threshold. */
|
||||||
|
@keyframes bar-blink {
|
||||||
|
0%, 52%, 100% { opacity: 0.1; }
|
||||||
|
6%, 44% { opacity: 1; }
|
||||||
|
}
|
||||||
|
@keyframes bar-shimmer { to { transform: translateX(-600px); } }
|
||||||
|
@keyframes bar-shimmer-back { to { transform: translateX(437px); } }
|
||||||
|
@keyframes bar-drop {
|
||||||
|
0% { opacity: 0; transform: translateX(calc(var(--nav-from-x, 0px) - 20px)) scale(0.4, 1); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1); }
|
||||||
|
18% { opacity: 1; transform: translateX(calc(var(--nav-from-x, 0px) - 20px)) scale(1.6, 1.4); animation-timing-function: cubic-bezier(0.7, 0, 0.25, 1); }
|
||||||
|
66% { opacity: 1; transform: translateX(calc(var(--nav-here-x, 0px) - 20px)) scale(2.6, 0.8); animation-timing-function: cubic-bezier(0.3, 0, 0.3, 1.4); }
|
||||||
|
80% { opacity: 1; transform: translateX(calc(var(--nav-here-x, 0px) - 20px)) scale(0.8, 1.2); }
|
||||||
|
100% { opacity: 0; transform: translateX(calc(var(--nav-here-x, 0px) - 20px)) scale(1); }
|
||||||
|
}
|
||||||
|
/* The ripple, spread flat: it widens along the bar but never grows down
|
||||||
|
into the exit numerals. */
|
||||||
|
@keyframes bar-ripple {
|
||||||
|
0% { opacity: 0; transform: scale(0.2); }
|
||||||
|
20% { opacity: 1; }
|
||||||
|
100% { opacity: 0; transform: scale(1.9, 1); }
|
||||||
|
}
|
||||||
|
@keyframes bar-ghost {
|
||||||
|
0%, 12% { opacity: 0; transform: translateX(calc(var(--nav-from-x, 0px) - 120px)); animation-timing-function: cubic-bezier(0.7, 0, 0.25, 1); }
|
||||||
|
40% { opacity: 0.8; transform: translateX(calc(var(--nav-here-x, 0px) - 120px)); }
|
||||||
|
100% { opacity: 0; transform: translateX(calc(var(--nav-here-x, 0px) - 120px)); }
|
||||||
|
}
|
||||||
|
@keyframes bar-glint {
|
||||||
|
0% { opacity: 0; transform: translateX(calc(var(--nav-here-x, 0px) - 14px)) scaleX(0.4); }
|
||||||
|
18% { opacity: 1; }
|
||||||
|
72% { opacity: 0.85; transform: translateX(calc(var(--nav-next-x, 0px) - 14px)) scaleX(1); }
|
||||||
|
100% { opacity: 0; transform: translateX(calc(var(--nav-next-x, 0px) - 14px)) scaleX(1); }
|
||||||
|
}
|
||||||
19
tests/unit/nav-bar-hue.test.ts
Normal file
19
tests/unit/nav-bar-hue.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { hueTier } from '@/composables/useNavBarSignals';
|
||||||
|
|
||||||
|
describe('nav bar hue tier', () => {
|
||||||
|
it('comes on at 80, 88 and 95 % of the dial', () => {
|
||||||
|
expect(hueTier(0, 0.79)).toBe(0);
|
||||||
|
expect(hueTier(0, 0.8)).toBe(1);
|
||||||
|
expect(hueTier(0, 0.88)).toBe(2);
|
||||||
|
expect(hueTier(0, 1)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('holds a tier until the speed falls below its own floor', () => {
|
||||||
|
expect(hueTier(1, 0.77)).toBe(1);
|
||||||
|
expect(hueTier(1, 0.75)).toBe(0);
|
||||||
|
expect(hueTier(3, 0.93)).toBe(3);
|
||||||
|
expect(hueTier(3, 0.9)).toBe(2);
|
||||||
|
expect(hueTier(3, 0.4)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user