commit 94832d861bd149f27954e2ab632d03f8fe28d018 Author: Bas Grolleman Date: Sun Nov 1 09:21:37 2026 +0100 Night Grid cyberpunk style guide Spec, stylesheet, glitch script, Claude Code skill and the design canvas source. Co-Authored-By: Claude Opus 5.5 diff --git a/NIGHT-GRID.md b/NIGHT-GRID.md new file mode 100644 index 0000000..ed3c44e --- /dev/null +++ b/NIGHT-GRID.md @@ -0,0 +1,100 @@ +# Night Grid style guide + +Instructions for building UIs (dashboards, tools, pages) in the Night Grid style: a dark neon HUD look in the spirit of near-future noir games. Dark ground, neon signal, hard edges. + +- Stylesheet: `night-grid.css` (same folder). Use its classes and `--ng-*` variables; don't redefine them. +- Script: `night-grid.js` (same folder, optional, `defer`). Drives the random glitch; nothing else needs JS. +- Visual reference: https://claude.ai/artifact/AT2mCxMkeLFzkife1rAuLk (private; the spec below is complete without it). + +## Principles + +1. **Signal over noise.** About 70% of the screen is near-black void. Data glows; the background doesn't. +2. **Hard edges.** No rounded corners, soft shadows or gradients. Panels get one 45° clipped corner (top-right) and 1px hairline borders. +3. **Color is meaning, never decoration.** Yellow = act/focus. Cyan = info and live data. Red = danger. Green = OK. +4. **Diegetic data.** Labels read like machine output: uppercase mono, IDs, timestamps, `//` separators (`01 // NETWORK`, `ALERT // DISK FULL`). + +## Colors + +| Token | Hex | Use | +|---|---|---| +| `--ng-void` | `#050508` | Page background | +| `--ng-panel` | `#0E1016` | Cards, panels | +| `--ng-raise` | `#1C2029` | Meter tracks, gridlines, hover | +| `--ng-line` | `#2A2F3A` | 1px borders, dividers | +| `--ng-text` | `#E6E8EC` | Primary text (never pure white) | +| `--ng-soft` | `#C3C8D1` | Secondary body text | +| `--ng-dim` | `#8A93A3` | Labels, axis text | +| `--ng-yellow` | `#FCEE0A` | Primary action, focus, page title | +| `--ng-cyan` | `#02D7F2` | Info, links, live data, chart series 1 | +| `--ng-red` | `#FF003C` | Danger fills/borders, thresholds | +| `--ng-red-txt` | `#FF3B5C` | Danger text (better contrast than `--ng-red`) | +| `--ng-green` | `#00FF9F` | Nominal/success, sparingly | + +Screen budget, roughly: void 70%, panel 15%, yellow 8%, cyan 4%, red 3%. + +## Typography + +- **UI and display:** Rajdhani 500/600/700. Headings and buttons uppercase; anything longer than one line in sentence case. +- **Data and labels:** JetBrains Mono 400/600, `font-variant-numeric: tabular-nums` for every metric. +- **Labels:** mono 12–13px, uppercase, `letter-spacing: 0.15em`, `--ng-dim`. +- **Scale (px):** 13, 15, 18 (body), 24, 32, 48, 72, 120. Body text at least 16px. +- Fonts: `https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=JetBrains+Mono:wght@400;600&display=swap` + +## Shape and line + +- Clipped corner: `clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%)`. 16px for cards, 24px for big sections, 12px for buttons. One cut per shape. +- Borders 1px `--ng-line`. Live/active dividers can be cyan. +- Selection or focus: yellow 2px corner brackets or a 2px yellow outline. +- Section headings can end in a broken yellow dash: a long line, then 8px, then 4px segments. +- **Diagonal structure:** edges step at 45°. Use `.ng-rule` under headings (thick bar stepping down into a hairline), `.ng-edge` for a stepped band along a section's bottom (`.ng-edge--hot` makes it yellow), and `.ng-panel--notched` for a panel with several cuts. One or two of these per screen; they frame, they don't fill. + +## Components (classes in `night-grid.css`) + +| Component | Class | Notes | +|---|---|---| +| Panel | `.ng-panel` | Clipped card | +| Button | `.ng-btn` + `--primary` / `--info` / `--danger` | Min height 48px. One `--primary` per view | +| Tag | `.ng-tag` + `--priority` / `--live` / `--ok` / `--breach` | Status chips | +| Meter | `.ng-meter` with 10 ``, lit ones get `.on` | Add `.ng-meter--danger` past a threshold | +| Alert | `.ng-alert` | Red border, 8% red fill. Say the thing, the number and the time | +| Input | `.ng-input` | Yellow underline on focus, red when `aria-invalid="true"` | +| Label | `.ng-label` | Mono machine label | +| Data | `.ng-data` | Mono, tabular numbers | +| Glow | `.ng-glow` | Only on the single most important value per screen | +| Fold panel | `
Title…
` | Yellow corner chip with `»`: points down when folded, up when open. Native, keyboard-accessible, no JS | +| Glitch | `.ng-glitch` | Marks a heading or value as a glitch candidate; `night-grid.js` glitches one at random every 6–14s | +| Scanlines | `.ng-scanlines` on `` | Optional; disabled with reduced motion | + +A stat tile is a `.ng-panel` holding a `.ng-label`, a big `.ng-data` value (about 40px, unit at 18px in `--ng-dim`), a `.ng-meter`, and a short status line. + +## Charts + +- Series 1 cyan, series 2 yellow, threshold/alert red. At most three series. +- Gridlines `--ng-raise`, axis labels mono 12px `--ng-dim`. No chart background. +- Square bars with 2px gaps. Lines 2px, no smoothing. + +## Motion + +- Fast and stepped: 120ms, `linear` or `steps(4)`. No easing bounces. +- Glitch (skew, RGB split, sliced, 360ms) on a state change, plus an ambient glitch: one random `.ng-glitch` element every ~10s. Put `.ng-glitch` on 3–6 headings or key values, never on body text or controls. +- Numbers tick up when they change; panels wipe in left to right. +- Respect `prefers-reduced-motion`: no animation, no scanlines. + +## Accessibility + +- Text contrast at least 4.5:1 (all text tokens above pass on `--ng-void` and `--ng-panel`). +- Never use color alone: every state also has a label or icon. +- Real ` + + + +
+
+

Event log

+
+
+
+ + + + + + + + + + + + + + + + + + + +
TIMELEVELSOURCEMESSAGE
{{l.t}}{{l.level}}{{l.src}}{{l.msg}}
+
+
+ + + + + + diff --git a/canvas/Main.dc.html b/canvas/Main.dc.html new file mode 100644 index 0000000..52d16a5 --- /dev/null +++ b/canvas/Main.dc.html @@ -0,0 +1,316 @@ + + + + +Night Grid style guide + + + + + + + + + +
+ + + +
+ +
+
+SYS.STYLE +REV 1.0 // 2026-10-31 +STATUS: ONLINE +
+

Night Grid

+

A visual language for dashboards and tools: dark streets, neon signal, hard edges. Inspired by the HUD look of near-future noir games, built to stay readable at 3 a.m.

+
+ +
+

01 // PRINCIPLES

+
+
+
Signal over noise
+

The city is dark so the data can glow. 70% of any screen is near-black void.

+
+
+
Hard edges
+

No rounded corners. Panels get one 45° clipped corner and 1px hairlines.

+
+
+
Color is meaning
+

Yellow = act or focus. Cyan = info and live data. Red = danger. Never decorative.

+
+
+
Diegetic data
+

Labels read like machine output: uppercase mono, IDs, timestamps, // separators.

+
+
+
+ +
+

02 // COLOR

+
+ +
+
+
+
{{c.name}}
+
{{c.hex}} · {{c.token}}
+
{{c.role}}
+
+
+
+
+
+
SCREEN BUDGET
+
+
+
+
+
+
+
+
+VOID 70%PANEL 15%YELLOW 8%CYAN 4%RED 3% +
+
+
+ +
+

03 // TYPE

+
+
+
DISPLAY + UI — RAJDHANI 500/600/700
+
Wake up
+
Section heading 32
+
Body 18/1.5. Condensed, squared shapes; uppercase for headings and buttons, sentence case for anything longer than a line.
+
+
+
DATA + LABELS — JETBRAINS MONO 400/600
+
98.6%
+
LABEL 13 · +0.15EM · UPPERCASE
+
[22:41:07] node-03 > sync complete
+
+
+
+SCALE 13 · 15 · 18 · 24 · 32 · 48 · 72 · 120TABULAR NUMBERS FOR ALL METRICS +
+
+ +
+

04 // SHAPE + LINE

+
+
+
+
Clipped panel
+
Top-right cut, 16px (cards) or 24px (sections). One cut per shape.
+
+
+
+
+
+
+
+
+
Target brackets
+
Marks focus or selection. Yellow only, 2px.
+
+
+
+
+
+
+
+
Hairlines
+
1px #2A2F3A dividers; cyan for live; broken yellow dash as a header tail.
+
+
+
+
+
+
Network + +
+
+
Stepped rule .ng-rule
+
Under headings: a thick bar that steps down at 45° into a hairline.
+
+
+
+ +
+
Edge band .ng-edge
+
Stepped band along a section's bottom. Line grey by default, yellow with --hot.
+
+
+
+
Notched panel .ng-panel--notched
+
Several cuts for a hero panel. One or two diagonal pieces per screen.
+
+
+
+ +
+

05 // COMPONENTS

+
+ + + + +
+
+PRIORITY +LIVE +NOMINAL +BREACH +IDLE +
+
+
+
STAT TILE // CPU LOAD
+
42%
+
+
+
+
▲ SEGMENTED METER · 10 STEPS
+
+
+
+ +ALERT // ICE BREACH +
+
Disk /dev/nvme0 at 94%
+
Alerts state the thing, the number and the time. Red border, 8% red fill.
+
+
+ + +
Focus = yellow underline. Errors swap it to red with a mono message.
+
+
+
+
+Fold panel · open +

Corner chip » points up: click to fold. Native <details>, works with the keyboard, no script.

+
+
+Fold panel · folded +

Folded, the chip points down: click to unfold.

+
+
+
+ +
+

06 // CHARTS + MOTION

+
+
    +
  • Series 1 cyan, series 2 yellow, threshold/alert red. Max three series per chart.
  • +
  • Gridlines #1C2029, axis labels mono 12px #8A93A3. No chart backgrounds.
  • +
  • Bars squared, 2px gaps. Lines 2px, no smoothing: the data is jagged, show it.
  • +
  • Glow only on the single most important value: text-shadow 0 0 12px at 60%.
  • +
+
    +
  • Motion is fast and stepped: 120ms, steps(4) or linear. No bounces.
  • +
  • Glitch (skew, RGB split, slices, 360ms) on state change, plus one random marked heading every 6–14s. Watch this page's title and principles.
  • +
  • Numbers tick up when they change; panels wipe in left to right.
  • +
  • Scanlines at 2.5% white, toggleable, off under prefers-reduced-motion.
  • +
+
+
+ +
+

07 // TOKENS

+
:root {
+  --ng-void:    #050508;  /* page ground */
+  --ng-panel:   #0E1016;  /* cards, panels */
+  --ng-raise:   #1C2029;  /* tracks, grid, hover */
+  --ng-line:    #2A2F3A;  /* 1px borders */
+  --ng-text:    #E6E8EC;
+  --ng-dim:     #8A93A3;  /* labels, 6.5:1 on void */
+  --ng-yellow:  #FCEE0A;  /* action, focus */
+  --ng-cyan:    #02D7F2;  /* info, live data */
+  --ng-red:     #FF003C;  /* danger fill/border */
+  --ng-red-txt: #FF3B5C;  /* danger text */
+  --ng-green:   #00FF9F;  /* ok */
+  --ng-font-ui:   'Rajdhani', sans-serif;
+  --ng-font-data: 'JetBrains Mono', monospace;
+  --ng-cut: 16px;         /* clip-path corner */
+}
+
+ +
+

08 // DO + DON'T

+
+
+
DO
+
One yellow call to action per view.
+
Pair every color state with a label or icon.
+
Keep body text sentence case and ≥16px.
+
Let empty void separate groups before adding lines.
+
+
+
DON'T
+
Use rounded corners, soft shadows or gradients.
+
Put yellow text on cyan or red (it vibrates).
+
Glow everything: if all of it shines, nothing does.
+
Copy game logos, fonts or UI assets: inspired by, never lifted.
+
+
+
+ +
+
+
+ + + diff --git a/canvas/canvas.json b/canvas/canvas.json new file mode 100644 index 0000000..d567593 --- /dev/null +++ b/canvas/canvas.json @@ -0,0 +1 @@ +{"v":3,"createdOnFiles":{"v":1,"at":"2026-10-31T12:00:00Z"},"title":"Night Grid Style Guide","launch":{"view":"canvas"},"pages":[],"boards":{"Main.dc.html":{"x":0,"y":0,"w":1440,"h":3600,"title":"Style guide","expand":"fill","is_interactive":true},"Dashboard.dc.html":{"x":1520,"y":0,"w":1440,"h":1100,"title":"Sample dashboard","expand":"fill"}},"order":["Main.dc.html","Dashboard.dc.html"],"notes":{},"designSystems":[]} diff --git a/night-grid.css b/night-grid.css new file mode 100644 index 0000000..0894d2a --- /dev/null +++ b/night-grid.css @@ -0,0 +1,228 @@ +/* Night Grid — dark neon HUD style for dashboards. + Guide: https://claude.ai/artifact/AT2mCxMkeLFzkife1rAuLk + Fonts: + Random glitch: */ + +:root { + --ng-void: #050508; /* page ground */ + --ng-panel: #0E1016; /* cards, panels */ + --ng-raise: #1C2029; /* tracks, grid, hover */ + --ng-line: #2A2F3A; /* 1px borders */ + --ng-text: #E6E8EC; + --ng-soft: #C3C8D1; /* secondary body text */ + --ng-dim: #8A93A3; /* labels */ + --ng-yellow: #FCEE0A; /* action, focus */ + --ng-cyan: #02D7F2; /* info, live data */ + --ng-red: #FF003C; /* danger fill/border */ + --ng-red-txt: #FF3B5C; /* danger text */ + --ng-green: #00FF9F; /* ok */ + --ng-font-ui: 'Rajdhani', sans-serif; + --ng-font-data: 'JetBrains Mono', monospace; + --ng-cut: 16px; + --ng-speed: 120ms; +} + +body { + margin: 0; + background: var(--ng-void); + color: var(--ng-text); + font: 500 18px/1.5 var(--ng-font-ui); +} + +a { color: var(--ng-cyan); } +a:hover { color: var(--ng-yellow); } +h1, h2, h3 { font-weight: 700; text-transform: uppercase; line-height: 1.1; } +h1 { color: var(--ng-yellow); } + +/* Mono machine-style label: "CPU LOAD", "01 // COLOR" */ +.ng-label { + font: 600 12px/1.4 var(--ng-font-data); + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ng-dim); +} +.ng-data { font-family: var(--ng-font-data); font-variant-numeric: tabular-nums; } + +/* Panel with one clipped top-right corner */ +.ng-panel { + background: var(--ng-panel); + border: 1px solid var(--ng-line); + padding: 20px 24px; + clip-path: polygon(0 0, calc(100% - var(--ng-cut)) 0, 100% var(--ng-cut), 100% 100%, 0 100%); +} + +/* Buttons: one primary (yellow) per view */ +.ng-btn { + min-height: 48px; + padding: 0 28px; + border: 1px solid var(--ng-line); + background: transparent; + color: var(--ng-dim); + font: 700 18px var(--ng-font-ui); + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: filter var(--ng-speed) linear; +} +.ng-btn:hover { filter: brightness(1.2); } +.ng-btn:focus-visible { outline: 2px solid var(--ng-yellow); outline-offset: 3px; } +.ng-btn--primary { + background: var(--ng-yellow); + color: var(--ng-void); + border: 0; + clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%); +} +.ng-btn--info { color: var(--ng-cyan); border-color: var(--ng-cyan); } +.ng-btn--danger { color: var(--ng-red-txt); border-color: var(--ng-red); } + +/* Tags */ +.ng-tag { + display: inline-block; + padding: 4px 10px; + border: 1px solid var(--ng-line); + color: var(--ng-dim); + font: 600 12px var(--ng-font-data); + letter-spacing: 0.12em; + text-transform: uppercase; +} +.ng-tag--priority { background: var(--ng-yellow); color: var(--ng-void); border-color: var(--ng-yellow); } +.ng-tag--live { color: var(--ng-cyan); border-color: var(--ng-cyan); } +.ng-tag--ok { color: var(--ng-green); border-color: var(--ng-green); } +.ng-tag--breach { background: var(--ng-red); color: #fff; border-color: var(--ng-red); } + +/* Segmented meter:
…10×
*/ +.ng-meter { display: flex; gap: 3px; } +.ng-meter > i { flex: 1; height: 6px; background: var(--ng-raise); } +.ng-meter > i.on { background: var(--ng-cyan); } +.ng-meter--danger > i.on { background: var(--ng-red); } + +/* Alert banner */ +.ng-alert { + background: rgb(255 0 60 / 0.08); + border: 1px solid var(--ng-red); + padding: 20px 24px; +} +.ng-alert .ng-label { color: var(--ng-red-txt); } + +/* Input: yellow underline on focus */ +.ng-input { + min-height: 48px; + padding: 0 16px; + background: var(--ng-void); + color: var(--ng-text); + border: 1px solid var(--ng-line); + font: 16px var(--ng-font-data); +} +.ng-input:focus { outline: none; border-bottom: 2px solid var(--ng-yellow); } +.ng-input[aria-invalid="true"] { border-bottom: 2px solid var(--ng-red); } + +/* Glow: the single most important value only */ +.ng-glow { text-shadow: 0 0 12px rgb(252 238 10 / 0.6); } + +/* --- Diagonal structure --- */ + +/* Multi-notch panel: cut top-left, step in the top edge, cut bottom-right */ +.ng-panel--notched { + clip-path: polygon(0 16px, 16px 0, 55% 0, calc(55% + 12px) 12px, 100% 12px, + 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%); + padding-top: 28px; +} + +/* Heading underline: thick bar that steps down diagonally into a hairline */ +.ng-rule { position: relative; padding-bottom: 16px; } +.ng-rule::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 8px; + background: var(--ng-rule, var(--ng-yellow)); + clip-path: polygon(0 0, 72px 0, 78px 5px, 100% 5px, 100% 6px, 76px 6px, 70px 8px, 0 8px); +} + +/* Stepped edge band along the bottom of a section */ +.ng-edge { position: relative; padding-bottom: 40px; } +.ng-edge::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 14px; + background: var(--ng-edge, var(--ng-line)); + clip-path: polygon(0 100%, 0 0, 22% 0, calc(22% + 14px) 100%, 52% 100%, calc(52% + 7px) 50%, + 70% 50%, calc(70% + 7px) 100%, 90% 100%, calc(90% + 14px) 0, 100% 0, 100% 100%); +} +.ng-edge--hot { --ng-edge: var(--ng-yellow); } + +/* --- Glitch --- */ +/* .ng-glitch marks candidates; night-grid.js adds .is-glitching to one at random every 6–14s */ +.ng-glitch.is-glitching { animation: ng-glitch 360ms steps(1, end); } +@keyframes ng-glitch { + 20% { transform: translateX(-3px) skewX(-12deg); text-shadow: 2px 0 var(--ng-red), -2px 0 var(--ng-cyan); clip-path: inset(10% 0 55% 0); } + 40% { transform: translateX(3px) skewX(8deg); clip-path: inset(60% 0 8% 0); } + 60% { transform: translateX(-1px); text-shadow: -2px 0 var(--ng-red), 2px 0 var(--ng-cyan); clip-path: inset(30% 0 35% 0); } + 80% { transform: none; text-shadow: 1px 0 var(--ng-red), -1px 0 var(--ng-cyan); } +} + +/* --- Fold / unfold --- +
Title…content…
+ Corner chip shows » pointing down (unfold) when closed, up (fold) when open. */ +.ng-fold { + position: relative; + background: var(--ng-panel); + border: 1px solid var(--ng-line); +} +.ng-fold > summary { + list-style: none; + cursor: pointer; + min-height: 44px; + padding: 10px 64px 10px 24px; + display: flex; + align-items: center; + font: 700 24px/1.1 var(--ng-font-ui); + text-transform: uppercase; +} +.ng-fold > summary::-webkit-details-marker { display: none; } +.ng-fold > summary:focus-visible { outline: 2px solid var(--ng-yellow); outline-offset: -2px; } +.ng-fold > summary::before { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 44px; + height: 44px; + background: var(--ng-yellow); + clip-path: polygon(0 0, 100% 0, 100% 100%, 12px 100%, 0 calc(100% - 12px)); +} +.ng-fold > summary::after { + content: "»"; + position: absolute; + top: 0; + right: 0; + width: 44px; + height: 44px; + display: grid; + place-items: center; + color: var(--ng-void); + font: 700 24px/1 var(--ng-font-data); + transform: rotate(90deg); + transition: transform var(--ng-speed) steps(2); +} +.ng-fold[open] > summary::after { transform: rotate(-90deg); } +.ng-fold > :not(summary) { margin: 0; padding: 0 24px 20px; } + +/* Scanline overlay: add class to */ +.ng-scanlines::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background: repeating-linear-gradient(0deg, rgb(255 255 255 / 0.025) 0 1px, transparent 1px 3px); +} + +@media (prefers-reduced-motion: reduce) { + .ng-scanlines::after { display: none; } + * { transition: none !important; animation: none !important; } +} diff --git a/night-grid.js b/night-grid.js new file mode 100644 index 0000000..538217f --- /dev/null +++ b/night-grid.js @@ -0,0 +1,15 @@ +// Night Grid: glitch one random .ng-glitch element every 6–14s (~10s average). +(() => { + if (matchMedia('(prefers-reduced-motion: reduce)').matches) return; + const next = () => setTimeout(tick, 6000 + Math.random() * 8000); + const tick = () => { + const els = document.querySelectorAll('.ng-glitch'); + const el = els[Math.floor(Math.random() * els.length)]; + if (el) { + el.classList.add('is-glitching'); + el.addEventListener('animationend', () => el.classList.remove('is-glitching'), { once: true }); + } + next(); + }; + next(); +})();