mirror of
https://github.com/Nexoniarz/Niveus.git
synced 2026-11-03 04:34:26 +00:00
Bevelled controls with light falling across them, fields as wells, solid windows, and glass kept for the chrome that floats over everything else. Warm graphite in dark, soft paper in light, one brand color driving both. Ships its own fonts (Instrument Sans, JetBrains Mono; OFL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
358 lines
11 KiB
CSS
358 lines
11 KiB
CSS
/* ==========================================================================
|
|
Niveus — Navigation
|
|
--------------------------------------------------------------------------
|
|
.nv-nav horizontal bar, frosted, sticky by default
|
|
.nv-nav--vertical sidebar
|
|
.nv-nav--plain solid instead of frosted
|
|
.nv-nav__brand / __list / __item / __section / __spacer
|
|
.nv-nav-tabs in-page tab strip with a sliding indicator
|
|
|
|
Navigation is chrome: it sits above content rather than in it. That is why
|
|
the bar is frosted and bevelled — the blur is not decoration, it is the
|
|
signal that content is passing underneath. A sidebar that scrolls with the page is not
|
|
chrome, so .nv-nav--vertical drops the translucency by default.
|
|
========================================================================== */
|
|
|
|
.nv-nav {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
z-index: 10;
|
|
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-4);
|
|
padding: var(--nv-space-2) var(--nv-space-4);
|
|
min-block-size: var(--nv-control-height-lg);
|
|
|
|
background-color: var(--nv-material-frosted-bg);
|
|
border-block-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
|
|
/* The travelling selection marker sits behind its item at z-index -1; the
|
|
bar has to be its own stacking context or the marker would fall behind
|
|
the bar's background and disappear. */
|
|
isolation: isolate;
|
|
box-shadow: var(--nv-bevel);
|
|
backdrop-filter: var(--nv-backdrop-frosted);
|
|
-webkit-backdrop-filter: var(--nv-backdrop-frosted);
|
|
transition: var(--nv-transition-surface);
|
|
}
|
|
|
|
/* The bar only earns a shadow once something is behind it. Set data-scrolled
|
|
from the application on scroll. */
|
|
.nv-nav[data-scrolled] {
|
|
box-shadow: var(--nv-bevel), var(--nv-elevation-2);
|
|
transition: box-shadow var(--nv-duration-normal) var(--nv-ease-standard);
|
|
}
|
|
|
|
.nv-nav--plain {
|
|
background-color: var(--nv-color-surface);
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
}
|
|
|
|
.nv-nav--static {
|
|
position: static;
|
|
}
|
|
|
|
/* A navigation group used inside content rather than as page chrome: no bar,
|
|
no border, no blur — just the items and their travelling selection. */
|
|
.nv-nav--inline {
|
|
position: static;
|
|
display: inline-flex;
|
|
min-block-size: 0;
|
|
padding: var(--nv-space-1);
|
|
|
|
background-color: transparent;
|
|
border-block-end: 0;
|
|
box-shadow: none;
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
}
|
|
|
|
.nv-nav__brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
font: var(--nv-font-title);
|
|
letter-spacing: var(--nv-letter-spacing-tight);
|
|
color: var(--nv-color-text-primary);
|
|
text-decoration: none;
|
|
padding-inline: var(--nv-space-1);
|
|
}
|
|
|
|
.nv-nav__list {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-1);
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
min-inline-size: 0;
|
|
}
|
|
|
|
.nv-nav__spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
/* --- Item --------------------------------------------------------------------
|
|
Items are flat until they are touched. The active item is the only one with
|
|
material, which keeps the bar quiet while still answering "where am I". */
|
|
|
|
.nv-nav__item {
|
|
/* Rides above a travelling marker once niveus.js has given it a name. */
|
|
view-transition-class: nv-carried;
|
|
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
padding: var(--nv-space-2) var(--nv-space-3);
|
|
min-block-size: var(--nv-control-height-md);
|
|
|
|
border: var(--nv-border-width) solid transparent;
|
|
border-radius: var(--nv-radius-md);
|
|
background-color: transparent;
|
|
|
|
color: var(--nv-color-text-secondary);
|
|
font: var(--nv-font-label);
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
transition: var(--nv-transition-control);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.nv-nav__item:hover {
|
|
background-color: var(--nv-color-surface-hover);
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
}
|
|
|
|
.nv-nav__item:active {
|
|
background-color: var(--nv-color-surface-active);
|
|
box-shadow: var(--nv-elevation-inset-sm);
|
|
transition-duration: var(--nv-duration-instant);
|
|
}
|
|
|
|
.nv-nav__item:focus-visible {
|
|
outline: none;
|
|
box-shadow: var(--nv-focus-ring);
|
|
}
|
|
|
|
/* Selection is a material that *moves*. The emphasis lives in a pseudo-element
|
|
rather than on the item itself, and carries a view-transition-name: when the
|
|
selection changes inside a transition, the browser interpolates the marker's
|
|
old box into its new one, so the highlight slides from the previous item to
|
|
the next instead of blinking out and in somewhere else.
|
|
|
|
Only one item in a group may be selected at a time — that is what keeps the
|
|
name unique. A page with several selection groups must give each its own
|
|
--nv-selection-name, or the duplicate name aborts the transition. */
|
|
|
|
/* Selection changes color and gains material — but not weight. Bolding the
|
|
selected item re-measures its text, which reflows every item beside it on
|
|
each change, and makes the old and new labels cross-fade against each other
|
|
at different widths. The pill is the cue; the weight was costing a flicker
|
|
for nothing. */
|
|
.nv-nav__item[aria-current],
|
|
.nv-nav__item--active,
|
|
.nv-nav__item[aria-selected="true"] {
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
|
|
.nv-nav__item[aria-current]::before,
|
|
.nv-nav__item--active::before,
|
|
.nv-nav__item[aria-selected="true"]::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: -1;
|
|
|
|
background-color: var(--nv-color-surface-elevated);
|
|
background-image: var(--nv-sheen-raised);
|
|
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
border-radius: inherit;
|
|
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
|
view-transition-class: nv-travelling;
|
|
}
|
|
|
|
/* The marker is named only while its own group is changing (niveus.js sets
|
|
data-nv-travelling for the length of the transition). A permanent name
|
|
would lift every marker on the page out of the page snapshot on every
|
|
transition, theme changes included. */
|
|
[data-nv-travelling] .nv-nav__item[aria-current]::before,
|
|
[data-nv-travelling] .nv-nav__item--active::before,
|
|
[data-nv-travelling] .nv-nav__item[aria-selected="true"]::before {
|
|
view-transition-name: var(--nv-selection-name);
|
|
}
|
|
|
|
/* Hovering an already-selected item should not restate the emphasis. */
|
|
@media (hover: hover) {
|
|
.nv-nav__item[aria-current]:hover,
|
|
.nv-nav__item--active:hover,
|
|
.nv-nav__item[aria-selected="true"]:hover {
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
|
|
.nv-nav__item[aria-disabled="true"] {
|
|
color: var(--nv-color-text-disabled);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.nv-nav__icon {
|
|
inline-size: 1.15em;
|
|
block-size: 1.15em;
|
|
flex: none;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.nv-nav__badge {
|
|
margin-inline-start: auto;
|
|
padding-inline: var(--nv-space-1-5);
|
|
min-inline-size: var(--nv-space-5);
|
|
block-size: var(--nv-space-5);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
|
|
background-color: var(--nv-color-accent-subtle);
|
|
color: var(--nv-color-accent-text);
|
|
border-radius: var(--nv-radius-full);
|
|
font: var(--nv-font-caption);
|
|
font-size: var(--nv-font-size-2xs);
|
|
font-weight: var(--nv-font-weight-semibold);
|
|
}
|
|
|
|
/* --- Vertical ------------------------------------------------------------------ */
|
|
|
|
.nv-nav--vertical {
|
|
position: sticky;
|
|
inset-block-start: 0;
|
|
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--nv-space-1);
|
|
inline-size: 15rem;
|
|
block-size: 100svh;
|
|
padding: var(--nv-space-4) var(--nv-space-3);
|
|
|
|
background-color: var(--nv-color-background-subtle);
|
|
backdrop-filter: none;
|
|
-webkit-backdrop-filter: none;
|
|
border-block-end: 0;
|
|
border-inline-end: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
box-shadow: none;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__list {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: var(--nv-space-0-5);
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__item {
|
|
justify-content: flex-start;
|
|
}
|
|
|
|
.nv-nav--vertical .nv-nav__brand {
|
|
padding: var(--nv-space-2) var(--nv-space-3);
|
|
margin-block-end: var(--nv-space-3);
|
|
}
|
|
|
|
/* Engraved, like .nv-overline: a section marks a region, it is not read. */
|
|
.nv-nav__section {
|
|
padding: var(--nv-space-4) var(--nv-space-3) var(--nv-space-1-5);
|
|
font: var(--nv-font-engraved);
|
|
letter-spacing: var(--nv-letter-spacing-engraved);
|
|
text-transform: uppercase;
|
|
color: var(--nv-color-text-tertiary);
|
|
}
|
|
|
|
.nv-nav__section:first-child {
|
|
padding-block-start: 0;
|
|
}
|
|
|
|
/* --- Tabs ------------------------------------------------------------------------
|
|
For switching views inside a page, not for moving between pages. The strip
|
|
itself is inset, so the selected tab reads as lifted out of the track. */
|
|
|
|
.nv-nav-tabs {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-0-5);
|
|
padding: var(--nv-space-1);
|
|
|
|
background-color: var(--nv-color-surface-inset);
|
|
border: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
|
border-radius: var(--nv-radius-md);
|
|
box-shadow: var(--nv-elevation-inset-md), var(--nv-ledge);
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
isolation: isolate;
|
|
}
|
|
|
|
.nv-nav-tabs::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.nv-nav-tabs .nv-nav__item {
|
|
border-radius: var(--nv-radius-sm);
|
|
min-block-size: var(--nv-control-height-sm);
|
|
padding-block: var(--nv-space-1);
|
|
}
|
|
|
|
/* Tabs are a selection group of their own, so they get their own marker name;
|
|
sharing the navigation bar's would mean two visible elements with the same
|
|
view-transition-name. */
|
|
.nv-nav-tabs {
|
|
--nv-selection-name: nv-tabs-selection;
|
|
}
|
|
|
|
.nv-nav-tabs .nv-nav__item[aria-selected="true"]::before {
|
|
border-radius: var(--nv-radius-sm);
|
|
}
|
|
|
|
/* --- Panels ----------------------------------------------------------------------
|
|
Switching tabs should feel like moving through one surface rather than
|
|
swapping the interface. nv.selectTab() gives the outgoing and incoming
|
|
panels one shared name for the length of the change, so they hand over
|
|
inside the same box — and slide with the direction of travel. The class is
|
|
what picks up the content animation in continuity.css. */
|
|
.nv-tab-panel {
|
|
view-transition-class: nv-content;
|
|
}
|
|
|
|
/* --- Breadcrumbs -------------------------------------------------------------------- */
|
|
|
|
.nv-breadcrumbs {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--nv-space-2);
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
font: var(--nv-font-caption);
|
|
color: var(--nv-color-text-tertiary);
|
|
}
|
|
|
|
.nv-breadcrumbs li + li::before {
|
|
content: "/";
|
|
margin-inline-end: var(--nv-space-2);
|
|
color: var(--nv-color-border-strong);
|
|
}
|
|
|
|
.nv-breadcrumbs a {
|
|
color: var(--nv-color-text-secondary);
|
|
text-decoration: none;
|
|
}
|
|
|
|
.nv-breadcrumbs a:hover {
|
|
color: var(--nv-color-text-primary);
|
|
}
|
|
|
|
.nv-breadcrumbs [aria-current] {
|
|
color: var(--nv-color-text-primary);
|
|
font-weight: var(--nv-font-weight-medium);
|
|
}
|