Niveus is the interface language the project uses from here on -- this site
first, applications after it. Rebuilding the site on it is what proves the
system is one, rather than a folder of CSS with a README.
**`style.css` is gone.** 21 KB that transcribed include/theme.h by hand:
palette, type scale, shadows, spacing, every component. The intent was right --
"a site describing an operating system should look like the operating system"
-- and the mechanism was a copy, which means two files that have to be kept in
step by remembering to. What replaced it is `app.css`, which is layout and
nothing else, and one declaration in each page:
:root { --nv-brand: #7C5CFF; }
That is SCH_ACCENT. Niveus derives the accent and its states, the link colour,
the focus ring, the tint in the greys and how far the status colours lean, for
light and dark both. One number to keep true instead of forty.
**The site is now adaptive.** Dark by default, because the OS is dark, with a
Light/Dark/Auto group in the bar that is real. The choice is kept in
localStorage and re-applied by an inline script before first paint, so the page
never renders in one appearance and corrects itself while you watch.
**PSOGM keeps its one purple, and now says so structurally.** #4B1D9A is
spelled the way Niveus spells a subtree with its own colour -- `data-nv-brand`
with its own `--nv-brand` on that section -- so every role inside re-derives
from it rather than being painted on. Still exactly one place, which is what
branding/README.md asks for.
Content is unchanged; what changed is what it is made of. Cards are `.nv-card`,
tags are `.nv-badge`, the support table is `.nv-table`, the crash section is a
`.nv-bed`, and the docs reader's two rails are `.nv-nav--vertical` with the
current document marked by `aria-current` rather than a class of our own.
`docs.js` emits `.nv-table` and `.nv-code`, so a change to the system reaches
the documentation too.
Three things were found by looking at it in a browser rather than by reading
it, and all three are the same mistake: **a `.nv-card` carries no padding of
its own -- its parts do.** The stat tiles, the "One ISO, one stick" band and
the card headers were all built from bare children, so numbers ran to the card
edge, the USB mark hung off it, and a title and its badge sat at opposite ends
of a row meant for a title and a trailing slot. Where the component had a right
way to say it, the markup now says it that way; where it did not -- a tile has
no parts -- `app.css` supplies the padding and says why.
Checked: every nv- class used across the three pages and docs.js exists in the
CSS, every token referenced by app.css is defined, and app.css contains no hex
value at all. Niveus's own tools/check.py passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
158 lines
8.9 KiB
CSS
158 lines
8.9 KiB
CSS
/* ==========================================================================
|
|
Niveus — Light appearance
|
|
--------------------------------------------------------------------------
|
|
Values for the light side of every semantic role. Nothing here is consumed
|
|
by components directly; tokens/color.css resolves these through light-dark().
|
|
|
|
Almost nothing below is a literal color. Each role is *derived* from
|
|
--nv-brand, so setting one value re-colours the entire system: the accent
|
|
and its states, what text goes on top of it, how the status colors lean, and
|
|
how much of the brand bleeds into the greys. See tokens/color.css for the
|
|
inputs.
|
|
|
|
Light Niveus reads as paper on a cool, slightly recessed canvas: surfaces are
|
|
near-white, depth comes from soft shadow rather than from contrast, and a
|
|
crisp top highlight gives raised elements their edge.
|
|
========================================================================== */
|
|
|
|
:root {
|
|
/* Follow the environment unless the application states an appearance. */
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
:root[data-theme="light"] {
|
|
color-scheme: light;
|
|
}
|
|
|
|
/* [data-nv-brand] re-derives the whole set for a subtree, so one region can
|
|
carry a different brand without a second theme. */
|
|
:root,
|
|
[data-nv-brand] {
|
|
/* --- Canvas and surfaces -----------------------------------------------
|
|
Paper stays paper: the two surfaces that hold content are left untinted so
|
|
text keeps its contrast. Everything the surfaces sit *in* picks up a trace
|
|
of the brand, which is what stops an accent from looking stuck on. */
|
|
--nv-light-background: color-mix(in oklab, #e9ecf3, var(--nv-brand) var(--nv-brand-tint));
|
|
--nv-light-background-subtle: color-mix(in oklab, #eff1f6, var(--nv-brand) var(--nv-brand-tint));
|
|
--nv-light-surface: var(--nv-palette-neutral-0);
|
|
--nv-light-surface-elevated: var(--nv-palette-neutral-0);
|
|
--nv-light-surface-inset: color-mix(in oklab, #e4e8f0, var(--nv-brand) var(--nv-brand-tint));
|
|
--nv-light-surface-hover: color-mix(in oklab, #f4f6fa, var(--nv-brand) var(--nv-brand-tint));
|
|
--nv-light-surface-active: color-mix(in oklab, #e8ebf2, var(--nv-brand) var(--nv-brand-tint));
|
|
--nv-light-scrim: rgb(16 21 30 / 0.32);
|
|
|
|
/* --- Text ---------------------------------------------------------------
|
|
Primary text stays neutral — it carries the reading contrast. The quieter
|
|
two lean towards the brand, where a tint reads as warmth rather than as a
|
|
loss of legibility. */
|
|
--nv-light-text-primary: var(--nv-palette-neutral-900);
|
|
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
|
|
--nv-light-text-tertiary: color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-brand) 8%);
|
|
--nv-light-text-disabled: var(--nv-palette-neutral-400);
|
|
--nv-light-text-inverted: var(--nv-palette-neutral-0);
|
|
|
|
/* --- Borders ------------------------------------------------------------ */
|
|
--nv-light-border-subtle: rgb(16 21 30 / 0.06);
|
|
--nv-light-border: rgb(16 21 30 / 0.11);
|
|
--nv-light-border-strong: rgb(16 21 30 / 0.2);
|
|
|
|
/* --- Accent --------------------------------------------------------------
|
|
Every filled color is pushed away from the middle of the lightness range
|
|
before it is used. A mid-tone is the one place where neither white nor
|
|
black text reaches 4.5:1, so a brand that lands there is darkened until
|
|
white works — or, if it is already light, lightened until black does. The
|
|
brand's hue and chroma are untouched; only its lightness moves, and only
|
|
as far as it has to. */
|
|
--nv-light-accent: oklch(from var(--nv-brand) calc(
|
|
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
|
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
|
) clamp(0.02, c, 0.25) h);
|
|
--nv-light-accent-hover: oklch(from var(--nv-light-accent) calc(l - 0.045) c h);
|
|
--nv-light-accent-active: oklch(from var(--nv-light-accent) calc(l - 0.09) c h);
|
|
--nv-light-accent-subtle: oklch(from var(--nv-light-accent) 0.965 calc(c * 0.16) h);
|
|
--nv-light-accent-subtle-hover: oklch(from var(--nv-light-accent) 0.93 calc(c * 0.22) h);
|
|
--nv-light-accent-border: oklch(from var(--nv-light-accent) l c h / 0.32);
|
|
--nv-light-accent-text: oklch(from var(--nv-light-accent) clamp(0.3, calc(l - 0.12), 0.5) c h);
|
|
/* Text that lands on the color itself. The clamp is a step function: below
|
|
the lightness threshold it resolves to 1 and the text goes near-white,
|
|
above it to 0 and the text goes near-black — so a yellow brand gets dark
|
|
labels and a navy one gets light labels, with no second token to set. */
|
|
--nv-light-on-accent: oklch(from var(--nv-light-accent)
|
|
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
|
calc(c * 0.2) h);
|
|
|
|
/* --- Success ------------------------------------------------------------- */
|
|
--nv-light-success: oklch(from
|
|
color-mix(in oklab, var(--nv-palette-success-500), var(--nv-brand) var(--nv-brand-harmony))
|
|
calc(
|
|
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
|
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
|
) clamp(0.02, c, 0.25) h);
|
|
--nv-light-success-hover: oklch(from var(--nv-light-success) calc(l - 0.05) c h);
|
|
--nv-light-success-subtle: oklch(from var(--nv-light-success) 0.965 calc(c * 0.16) h);
|
|
--nv-light-success-border: oklch(from var(--nv-light-success) l c h / 0.3);
|
|
--nv-light-success-text: oklch(from var(--nv-light-success) clamp(0.3, calc(l - 0.1), 0.48) c h);
|
|
--nv-light-on-success: oklch(from var(--nv-light-success)
|
|
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
|
calc(c * 0.2) h);
|
|
|
|
/* --- Warning -------------------------------------------------------------- */
|
|
/* Amber starts above the pivot on purpose. Pushed the other way it lands as
|
|
brown, which reads as neither a warning nor a color anyone chose. */
|
|
--nv-light-warning: oklch(from
|
|
color-mix(in oklab, var(--nv-palette-warning-400), var(--nv-brand) var(--nv-brand-harmony))
|
|
calc(
|
|
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
|
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
|
) clamp(0.02, c, 0.25) h);
|
|
--nv-light-warning-hover: oklch(from var(--nv-light-warning) calc(l - 0.05) c h);
|
|
--nv-light-warning-subtle: oklch(from var(--nv-light-warning) 0.965 calc(c * 0.16) h);
|
|
--nv-light-warning-border: oklch(from var(--nv-light-warning) l c h / 0.32);
|
|
--nv-light-warning-text: oklch(from var(--nv-light-warning) clamp(0.3, calc(l - 0.1), 0.48) c h);
|
|
--nv-light-on-warning: oklch(from var(--nv-light-warning)
|
|
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
|
calc(c * 0.2) h);
|
|
|
|
/* --- Danger ---------------------------------------------------------------- */
|
|
--nv-light-danger: oklch(from
|
|
color-mix(in oklab, var(--nv-palette-danger-500), var(--nv-brand) var(--nv-brand-harmony))
|
|
calc(
|
|
clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1) * min(l, var(--nv-contrast-dark)) +
|
|
(1 - clamp(0, (var(--nv-contrast-pivot) - l) * 100, 1)) * max(l, var(--nv-contrast-light))
|
|
) clamp(0.02, c, 0.25) h);
|
|
--nv-light-danger-hover: oklch(from var(--nv-light-danger) calc(l - 0.05) c h);
|
|
--nv-light-danger-active: oklch(from var(--nv-light-danger) calc(l - 0.1) c h);
|
|
--nv-light-danger-subtle: oklch(from var(--nv-light-danger) 0.965 calc(c * 0.16) h);
|
|
--nv-light-danger-border: oklch(from var(--nv-light-danger) l c h / 0.3);
|
|
--nv-light-danger-text: oklch(from var(--nv-light-danger) clamp(0.3, calc(l - 0.1), 0.48) c h);
|
|
--nv-light-on-danger: oklch(from var(--nv-light-danger)
|
|
calc(0.14 + clamp(0, (0.68 - l) * 100, 1) * 0.84)
|
|
calc(c * 0.2) h);
|
|
|
|
/* --- Depth and material -------------------------------------------------
|
|
Three shadow colors build every elevation level: an ambient contact
|
|
shadow, a soft penumbra, and a deeper umbra for high elevations. */
|
|
--nv-light-shadow-ambient: rgb(16 21 30 / 0.05);
|
|
--nv-light-shadow-penumbra: rgb(16 21 30 / 0.08);
|
|
--nv-light-shadow-umbra: rgb(16 21 30 / 0.06);
|
|
--nv-light-shadow-inset: rgb(16 21 30 / 0.12);
|
|
--nv-light-highlight: rgb(255 255 255 / 0.85);
|
|
--nv-light-highlight-strong: rgb(255 255 255 / 1);
|
|
|
|
/* Light surfaces are already lighter than their canvas; the shadow alone
|
|
carries elevation, so there is nothing to add. */
|
|
--nv-light-elevation-tint-1: transparent;
|
|
--nv-light-elevation-tint-2: transparent;
|
|
--nv-light-elevation-tint-3: transparent;
|
|
--nv-light-elevation-tint-4: transparent;
|
|
--nv-light-elevation-tint-5: transparent;
|
|
|
|
--nv-light-material-frosted: rgb(255 255 255 / 0.72);
|
|
--nv-light-material-glass: rgb(255 255 255 / 0.5);
|
|
--nv-light-material-border: rgb(255 255 255 / 0.55);
|
|
|
|
/* --- Interaction ----------------------------------------------------------- */
|
|
--nv-light-focus-ring: oklch(from var(--nv-light-accent) l c h / 0.4);
|
|
--nv-light-focus-ring-contrast: rgb(255 255 255 / 0.9);
|
|
--nv-light-selection: oklch(from var(--nv-light-accent) l c h / 0.2);
|
|
}
|