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/route.css';
|
||||
import './styles/gantry.css';
|
||||
import './styles/nav-bar.css';
|
||||
import './styles/cards.css';
|
||||
import './styles/maintenance.css';
|
||||
import './styles/print.css';
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { BeamSample } from "@/journey/headlight-beam";
|
||||
import type { JourneySectionsBinding } from "@/composables/useJourneySections";
|
||||
import { useSpeedometer } from "@/composables/useSpeedometer";
|
||||
import { SIGN_ACCENTS } from "@/journey/sign-routes";
|
||||
import { useNavBarSignals } from "@/composables/useNavBarSignals";
|
||||
|
||||
const props = defineProps<{
|
||||
items: ReadonlyArray<{ id: SectionId; label: string }>;
|
||||
@@ -25,6 +26,14 @@ const props = defineProps<{
|
||||
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>> = {
|
||||
start: "Start",
|
||||
profile: "Profile",
|
||||
@@ -159,6 +168,7 @@ const speedometer = useSpeedometer({
|
||||
return `${route} \u00b7 ${exit} \u00b7 ${ahead}`;
|
||||
},
|
||||
});
|
||||
useNavBarSignals(speedo, activeIndex);
|
||||
const speedoLabel = computed(() => (speedometer.units.value === "mph"
|
||||
? "Speedometer in miles per hour. Switch to kilometres per hour"
|
||||
: "Speedometer in kilometres per hour. Switch to miles per hour"));
|
||||
@@ -172,6 +182,8 @@ const navigate = (event: MouseEvent): void => {
|
||||
|
||||
<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'] }">
|
||||
<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
|
||||
ref="speedo"
|
||||
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