DIY — Blueprint Grid Background
A drafting-table blueprint background built entirely from layered CSS gradients — zero images, zero libraries. A full-bleed grid sits behind a sample hero sheet while a live tuner adjusts fine-grid size, major-line interval and line opacity, switches between navy, kraft, dark-shop and cyan-print themes, and toggles vignette, paper grain and a slow drift animation. Every control writes a custom property on :root, and a live code panel emits the exact CSS with a copy button.
MCP
Code
:root {
/* DIY baseline palette */
--blueprint: #0f2a4a;
--blueprint-d: #0a1e36;
--blueprint-l: #16385f;
--grid: rgba(140, 180, 220, 0.14);
--grid-major: rgba(140, 180, 220, 0.26);
--orange: #ff6b35;
--orange-d: #e0521f;
--orange-soft: #ffe3d6;
--kraft: #d9c7a7;
--kraft-l: #efe6d4;
--ink: #1c2733;
--ink-2: #3d4a57;
--muted: #6b7885;
--bg: #f4f1ea;
--white: #ffffff;
--line: rgba(28, 39, 51, 0.12);
--line-2: rgba(28, 39, 51, 0.2);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--sh-1: 0 1px 2px rgba(10, 30, 54, 0.16);
--sh-2: 0 10px 28px -12px rgba(10, 30, 54, 0.45);
--sh-3: 0 28px 70px -30px rgba(10, 30, 54, 0.7);
/* live-tuned grid variables */
--g-fine: 8px;
--g-major: 80px;
--g-op: 0.14;
--g-op-major: 0.26;
--g-rgb: 140, 180, 220;
--g-base: #0f2a4a;
--g-base-2: #0a1e36;
--g-drift: paused;
--g-vignette: 1;
--g-grain: 0.5;
/* surface tokens flipped per theme */
--fg: #e8f0f9;
--fg-dim: rgba(232, 240, 249, 0.68);
--card: rgba(10, 30, 54, 0.62);
--card-line: rgba(140, 180, 220, 0.28);
--accent: var(--orange);
}
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
min-height: 100vh;
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.5;
color: var(--fg);
background: var(--g-base-2);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mono { font-family: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace; }
/* ---------- THEMES ---------- */
body[data-theme="navy"] {
--g-base: #0f2a4a;
--g-base-2: #0a1e36;
--g-rgb: 140, 180, 220;
--fg: #e8f0f9;
--fg-dim: rgba(232, 240, 249, 0.68);
--card: rgba(10, 30, 54, 0.66);
--card-line: rgba(140, 180, 220, 0.28);
--accent: #ff6b35;
}
body[data-theme="kraft"] {
--g-base: #d9c7a7;
--g-base-2: #c9b48f;
--g-rgb: 90, 66, 38;
--fg: #2a2013;
--fg-dim: rgba(42, 32, 19, 0.7);
--card: rgba(239, 230, 212, 0.82);
--card-line: rgba(90, 66, 38, 0.26);
--accent: #b5471c;
}
body[data-theme="dark"] {
--g-base: #0b0d10;
--g-base-2: #06080a;
--g-rgb: 90, 220, 235;
--fg: #e6f6f8;
--fg-dim: rgba(230, 246, 248, 0.62);
--card: rgba(12, 16, 20, 0.72);
--card-line: rgba(90, 220, 235, 0.24);
--accent: #38dcea;
}
body[data-theme="cyan"] {
--g-base: #0a6d8f;
--g-base-2: #075a77;
--g-rgb: 255, 255, 255;
--fg: #f2fbff;
--fg-dim: rgba(242, 251, 255, 0.74);
--card: rgba(7, 90, 119, 0.66);
--card-line: rgba(255, 255, 255, 0.32);
--accent: #ffd166;
}
/* ---------- THE BACKGROUND SYSTEM ---------- */
.bp-canvas {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
background:
radial-gradient(120% 90% at 50% -10%, var(--g-base) 0%, var(--g-base-2) 72%),
var(--g-base-2);
}
.bp-layer { position: absolute; inset: -20%; }
.bp-grid {
background-image:
linear-gradient(to right, rgba(var(--g-rgb), var(--g-op-major)) 1px, transparent 1px),
linear-gradient(to bottom, rgba(var(--g-rgb), var(--g-op-major)) 1px, transparent 1px),
linear-gradient(to right, rgba(var(--g-rgb), var(--g-op)) 1px, transparent 1px),
linear-gradient(to bottom, rgba(var(--g-rgb), var(--g-op)) 1px, transparent 1px);
background-size:
var(--g-major) var(--g-major),
var(--g-major) var(--g-major),
var(--g-fine) var(--g-fine),
var(--g-fine) var(--g-fine);
background-position: 0 0;
animation: bp-drift 40s linear infinite;
animation-play-state: var(--g-drift);
will-change: background-position;
}
@keyframes bp-drift {
to { background-position: var(--g-major) var(--g-major), var(--g-major) var(--g-major), var(--g-major) var(--g-major), var(--g-major) var(--g-major); }
}
.bp-grain {
opacity: var(--g-grain);
background-image:
radial-gradient(rgba(var(--g-rgb), 0.10) 0.5px, transparent 0.6px),
radial-gradient(rgba(var(--g-rgb), 0.07) 0.5px, transparent 0.6px);
background-size: 3px 3px, 7px 7px;
background-position: 0 0, 2px 3px;
mix-blend-mode: overlay;
}
.bp-vignette {
opacity: var(--g-vignette);
background: radial-gradient(120% 85% at 50% 40%, transparent 40%, rgba(0, 0, 0, 0.5) 100%);
}
/* ---------- CHROME ---------- */
.skip {
position: absolute; left: 12px; top: -60px;
background: var(--accent); color: #10161d; padding: 10px 16px;
border-radius: var(--r-sm); font-weight: 700; z-index: 50; transition: top .18s;
}
.skip:focus { top: 12px; }
:where(a, button, input, [tabindex]):focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
border-radius: var(--r-sm);
}
.topbar {
display: flex; align-items: center; gap: 20px;
padding: 18px clamp(16px, 4vw, 48px);
border-bottom: 1px solid var(--card-line);
backdrop-filter: blur(6px);
background: linear-gradient(to bottom, rgba(0, 0, 0, .18), transparent);
position: sticky; top: 0; z-index: 20;
}
.brand { display: flex; align-items: center; gap: 12px; margin-right: auto; }
.brand-mark { width: 34px; height: 34px; color: var(--accent); display: block; }
.brand-mark svg { width: 100%; height: 100%; }
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-text strong { font-size: 15px; font-weight: 800; letter-spacing: .01em; }
.brand-text em {
font-style: normal; font-size: 11px; letter-spacing: .14em;
text-transform: uppercase; color: var(--fg-dim);
font-family: "JetBrains Mono", monospace;
}
.topnav { display: flex; gap: 4px; }
.topnav a {
color: var(--fg-dim); text-decoration: none; font-size: 13px; font-weight: 600;
padding: 8px 12px; border-radius: var(--r-sm); transition: background .16s, color .16s;
}
.topnav a:hover { color: var(--fg); background: rgba(var(--g-rgb), .12); }
.rev {
font-size: 11px; letter-spacing: .08em; color: var(--fg-dim);
border: 1px solid var(--card-line); padding: 5px 10px; border-radius: 999px;
}
main { padding: clamp(24px, 5vw, 56px) clamp(16px, 4vw, 48px) 40px; max-width: 1180px; margin: 0 auto; }
/* ---------- HERO ---------- */
.hero { display: grid; grid-template-columns: minmax(0, 1.65fr) minmax(0, 1fr); gap: 20px; align-items: start; }
.hero-card, .hero-side, .panel, .preset, .ledger table {
background: var(--card);
border: 1px solid var(--card-line);
border-radius: var(--r-lg);
backdrop-filter: blur(10px);
box-shadow: var(--sh-3);
}
.hero-card { padding: clamp(22px, 3.5vw, 38px); position: relative; overflow: hidden; }
.hero-card::before {
content: ""; position: absolute; inset: 10px; border-radius: var(--r-md);
border: 1px dashed rgba(var(--g-rgb), .22); pointer-events: none;
}
.hero-head { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.chip {
font-size: 11px; font-weight: 600; padding: 5px 10px; border-radius: 999px;
border: 1px solid var(--card-line); color: var(--fg-dim);
}
.chip b { color: var(--fg); }
.chip-orange { background: var(--accent); color: #12181f; border-color: transparent; font-weight: 700; }
.hero-card h1 {
margin: 0 0 12px; font-size: clamp(28px, 4.4vw, 46px); line-height: 1.08;
font-weight: 800; letter-spacing: -0.025em; max-width: 18ch;
}
.lede { margin: 0 0 24px; color: var(--fg-dim); font-size: clamp(14px, 1.3vw, 16px); max-width: 58ch; }
.lede code, .panel-head code {
font-family: "JetBrains Mono", monospace; font-size: .88em;
background: rgba(var(--g-rgb), .14); padding: 2px 6px; border-radius: 4px; color: var(--fg);
}
.specs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin: 0 0 26px; }
.specs > div {
border: 1px solid var(--card-line); border-radius: var(--r-md);
padding: 10px 12px; background: rgba(var(--g-rgb), .06);
}
.specs dt { font-size: 10px; text-transform: uppercase; letter-spacing: .12em; color: var(--fg-dim); }
.specs dd { margin: 4px 0 0; font-size: 15px; font-weight: 700; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.btn {
font: inherit; font-weight: 700; font-size: 14px; cursor: pointer;
padding: 11px 20px; border-radius: 999px; border: 1px solid transparent;
transition: transform .14s ease, box-shadow .18s ease, background .18s;
}
.btn:active { transform: translateY(1px) scale(.985); }
.btn-primary { background: var(--accent); color: #12181f; box-shadow: var(--sh-2); }
.btn-primary:hover { filter: brightness(1.07); box-shadow: 0 14px 30px -12px rgba(0, 0, 0, .6); }
.btn-ghost { background: transparent; color: var(--fg); border-color: var(--card-line); }
.btn-ghost:hover { background: rgba(var(--g-rgb), .12); }
.hero-side { padding: 22px; }
.side-title {
margin: 0 0 14px; font-size: 11px; letter-spacing: .16em; text-transform: uppercase;
color: var(--fg-dim); font-family: "JetBrains Mono", monospace;
}
.notes { list-style: none; margin: 0 0 20px; padding: 0; display: grid; gap: 11px; }
.notes li {
font-size: 13px; color: var(--fg-dim); display: grid;
grid-template-columns: 26px 1fr; gap: 8px; align-items: baseline;
}
.notes li span { color: var(--accent); font-size: 11px; font-weight: 700; }
.notes li b { color: var(--fg); }
.stamp {
display: grid; place-items: center; gap: 2px; padding: 12px;
border: 2px solid var(--accent); color: var(--accent);
border-radius: var(--r-sm); transform: rotate(-3deg);
font-weight: 800; letter-spacing: .18em; font-size: 13px; opacity: .9;
}
.stamp .mono { font-size: 10px; letter-spacing: .1em; font-weight: 600; }
/* ---------- PANEL ---------- */
.panel-wrap { margin-top: 28px; scroll-margin-top: 90px; }
.panel { overflow: hidden; }
.panel-head { padding: 22px clamp(18px, 3vw, 30px) 0; }
.panel-head h2 { margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -.02em; }
.panel-head p { margin: 6px 0 0; color: var(--fg-dim); font-size: 13px; }
.panel-body {
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 22px; padding: 22px clamp(18px, 3vw, 30px) 26px;
}
.control-col { display: grid; gap: 18px; align-content: start; }
.fld { border: 1px solid var(--card-line); border-radius: var(--r-md); padding: 14px 16px 18px; margin: 0; }
.fld legend {
font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
color: var(--fg-dim); padding: 0 8px; font-family: "JetBrains Mono", monospace;
}
.themes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.theme {
font: inherit; cursor: pointer; text-align: left; padding: 10px;
background: rgba(var(--g-rgb), .05); color: var(--fg);
border: 1px solid var(--card-line); border-radius: var(--r-md);
display: grid; gap: 2px; transition: border-color .16s, background .16s, transform .14s;
}
.theme:hover { background: rgba(var(--g-rgb), .13); transform: translateY(-1px); }
.theme[aria-checked="true"] { border-color: var(--accent); background: rgba(var(--g-rgb), .16); }
.sw { display: block; width: 100%; height: 22px; border-radius: var(--r-sm); margin-bottom: 5px; border: 1px solid rgba(0,0,0,.2); }
.sw-navy { background: linear-gradient(#16385f, #0a1e36); }
.sw-kraft { background: linear-gradient(#efe6d4, #c9b48f); }
.sw-dark { background: linear-gradient(#14181d, #06080a); }
.sw-cyan { background: linear-gradient(#0a8fb8, #075a77); }
.theme-name { font-size: 12.5px; font-weight: 700; }
.theme-sub { font-size: 10px; color: var(--fg-dim); }
.row { display: flex; justify-content: space-between; align-items: baseline; margin: 14px 0 6px; }
.row label { font-size: 13px; font-weight: 600; }
.row output { font-size: 12px; color: var(--accent); font-weight: 700; }
input[type="range"] {
-webkit-appearance: none; appearance: none; width: 100%; height: 4px;
background: rgba(var(--g-rgb), .28); border-radius: 999px; outline-offset: 6px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 18px; height: 18px;
border-radius: 50%; background: var(--accent); border: 3px solid var(--g-base-2);
box-shadow: var(--sh-1); cursor: pointer; transition: transform .14s;
}
input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.14); }
input[type="range"]::-moz-range-thumb {
width: 18px; height: 18px; border-radius: 50%; background: var(--accent);
border: 3px solid var(--g-base-2); cursor: pointer;
}
.toggles { display: grid; gap: 10px; }
.tgl { display: flex; align-items: center; gap: 10px; font-size: 13px; font-weight: 600; cursor: pointer; }
.tgl input { position: absolute; opacity: 0; width: 0; height: 0; }
.tgl-ui {
width: 38px; height: 21px; border-radius: 999px; flex: none;
background: rgba(var(--g-rgb), .22); border: 1px solid var(--card-line);
position: relative; transition: background .18s;
}
.tgl-ui::after {
content: ""; position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
border-radius: 50%; background: var(--fg); transition: transform .18s ease;
}
.tgl input:checked + .tgl-ui { background: var(--accent); }
.tgl input:checked + .tgl-ui::after { transform: translateX(17px); background: #12181f; }
.tgl input:focus-visible + .tgl-ui { outline: 2px solid var(--accent); outline-offset: 3px; }
.tgl input:disabled + .tgl-ui { opacity: .45; cursor: not-allowed; }
.hint { margin: 12px 0 0; font-size: 11.5px; color: var(--warn); }
/* code */
.code-col { display: flex; flex-direction: column; min-width: 0; }
.code-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.code-head .mono { font-size: 11px; color: var(--fg-dim); letter-spacing: .08em; }
.btn-copy {
padding: 7px 14px; font-size: 12px; background: rgba(var(--g-rgb), .14);
color: var(--fg); border-color: var(--card-line);
}
.btn-copy:hover { background: rgba(var(--g-rgb), .24); }
.code {
margin: 0; flex: 1; min-height: 260px; overflow: auto;
background: rgba(0, 0, 0, .34); border: 1px solid var(--card-line);
border-radius: var(--r-md); padding: 16px;
}
.code code {
font-family: "JetBrains Mono", monospace; font-size: 12px; line-height: 1.65;
white-space: pre; color: var(--fg); display: block;
}
.code-foot { margin: 10px 0 0; font-size: 11.5px; color: var(--fg-dim); }
/* ---------- PRESETS ---------- */
.presets { margin-top: 36px; scroll-margin-top: 90px; }
.sec-head { margin-bottom: 16px; }
.sec-head h2 { margin: 0; font-size: 22px; font-weight: 800; letter-spacing: -.02em; }
.sec-head p { margin: 5px 0 0; color: var(--fg-dim); font-size: 13px; }
.preset-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.preset { overflow: hidden; transition: transform .18s ease, border-color .18s; }
.preset:hover { transform: translateY(-3px); border-color: var(--accent); }
.preset-fill {
height: 132px;
background-color: var(--g-base);
background-image:
linear-gradient(to right, rgba(var(--g-rgb), calc(var(--p-op) * 1.9)) 1px, transparent 1px),
linear-gradient(to bottom, rgba(var(--g-rgb), calc(var(--p-op) * 1.9)) 1px, transparent 1px),
linear-gradient(to right, rgba(var(--g-rgb), var(--p-op)) 1px, transparent 1px),
linear-gradient(to bottom, rgba(var(--g-rgb), var(--p-op)) 1px, transparent 1px);
background-size:
var(--p-major) var(--p-major), var(--p-major) var(--p-major),
var(--p-fine) var(--p-fine), var(--p-fine) var(--p-fine);
border-bottom: 1px solid var(--card-line);
}
.preset[data-preset="divider"] .preset-fill {
height: 92px;
-webkit-mask-image: linear-gradient(to bottom, transparent, #000 45%, transparent);
mask-image: linear-gradient(to bottom, transparent, #000 45%, transparent);
}
.preset-meta { padding: 16px 18px 20px; }
.badge {
display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: .12em;
text-transform: uppercase; padding: 4px 9px; border-radius: 999px;
background: rgba(var(--g-rgb), .16); border: 1px solid var(--card-line); color: var(--fg-dim);
}
.preset-meta h3 { margin: 10px 0 6px; font-size: 16px; font-weight: 700; }
.preset-meta p { margin: 0 0 10px; font-size: 13px; color: var(--fg-dim); }
.tiny { font-size: 11px; color: var(--accent); font-weight: 600; }
/* ---------- LEDGER ---------- */
.ledger { margin-top: 32px; overflow-x: auto; }
.ledger table { width: 100%; border-collapse: collapse; min-width: 520px; }
.ledger caption {
text-align: left; padding: 16px 18px 10px; font-size: 12px;
color: var(--fg-dim); letter-spacing: .06em;
}
.ledger th, .ledger td { padding: 11px 18px; text-align: left; font-size: 13px; border-top: 1px solid var(--card-line); }
.ledger th { font-size: 10px; text-transform: uppercase; letter-spacing: .14em; color: var(--fg-dim); font-weight: 700; }
.ledger tbody tr:hover { background: rgba(var(--g-rgb), .08); }
.foot {
display: flex; flex-wrap: wrap; gap: 10px; justify-content: space-between;
padding: 22px clamp(16px, 4vw, 48px) 34px; max-width: 1180px; margin: 0 auto;
border-top: 1px solid var(--card-line); font-size: 12px; color: var(--fg-dim);
}
/* ---------- TOAST ---------- */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 22px);
background: var(--ink); color: #fff; padding: 12px 20px; border-radius: 999px;
font-size: 13px; font-weight: 600; box-shadow: var(--sh-3);
opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; z-index: 60;
border: 1px solid rgba(255, 255, 255, .14);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- RESPONSIVE ---------- */
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; }
.panel-body { grid-template-columns: 1fr; }
.preset-grid { grid-template-columns: 1fr; }
}
@media (max-width: 520px) {
.topnav { display: none; }
.topbar { padding: 14px 16px; gap: 12px; }
main { padding: 20px 14px 30px; }
.hero-card { padding: 20px 18px; }
.hero-card::before { inset: 6px; }
.hero-card h1 { font-size: 27px; }
.specs { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.themes { grid-template-columns: 1fr; }
.btn { width: 100%; }
.code { min-height: 200px; padding: 12px; }
.code code { font-size: 11px; }
.foot { flex-direction: column; }
.rev { display: none; }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.bp-grid { animation: none !important; }
*, *::before, *::after { transition-duration: .01ms !important; }
}(() => {
"use strict";
const root = document.documentElement;
const body = document.body;
const $ = (s, c = document) => c.querySelector(s);
const $$ = (s, c = document) => Array.from(c.querySelectorAll(s));
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const THEMES = {
navy: { label: "Navy blueprint", base: "#0f2a4a", base2: "#0a1e36", rgb: "140, 180, 220" },
kraft: { label: "Kraft paper", base: "#d9c7a7", base2: "#c9b48f", rgb: "90, 66, 38" },
dark: { label: "Dark shop", base: "#0b0d10", base2: "#06080a", rgb: "90, 220, 235" },
cyan: { label: "Cyan-print", base: "#0a6d8f", base2: "#075a77", rgb: "255, 255, 255" }
};
const state = {
theme: "navy",
fine: 8,
major: 80,
opacity: 0.14,
vignette: true,
drift: false,
grain: true
};
/* ---------- toast ---------- */
const toastEl = $("#toast");
let toastTimer = 0;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2200);
}
/* ---------- render ---------- */
function majorOpacity() {
return Math.min(0.9, +(state.opacity * 1.9).toFixed(3));
}
function apply() {
const t = THEMES[state.theme];
root.style.setProperty("--g-fine", state.fine + "px");
root.style.setProperty("--g-major", state.major + "px");
root.style.setProperty("--g-op", String(state.opacity));
root.style.setProperty("--g-op-major", String(majorOpacity()));
root.style.setProperty("--g-rgb", t.rgb);
root.style.setProperty("--g-base", t.base);
root.style.setProperty("--g-base-2", t.base2);
root.style.setProperty("--g-vignette", state.vignette ? "1" : "0");
root.style.setProperty("--g-grain", state.grain ? "0.5" : "0");
const driftOn = state.drift && !reduceMotion.matches;
root.style.setProperty("--g-drift", driftOn ? "running" : "paused");
body.dataset.theme = state.theme;
$$("[data-theme-btn]").forEach((b) =>
b.setAttribute("aria-checked", String(b.dataset.themeBtn === state.theme))
);
$("#fineOut").value = state.fine + "px";
$("#majorOut").value = state.major + "px";
$("#opacityOut").value = state.opacity.toFixed(2);
$$('[data-out="fine"]').forEach((n) => (n.textContent = state.fine + "px"));
$$('[data-out="major"]').forEach((n) => (n.textContent = state.major + "px"));
$$('[data-out="opacity"]').forEach((n) => (n.textContent = state.opacity.toFixed(2)));
const multi = $('[data-out="major-multi"]');
if (multi) multi.textContent = (state.major / state.fine).toFixed(state.major % state.fine ? 1 : 0);
renderCode();
}
function buildCSS() {
const t = THEMES[state.theme];
const mo = majorOpacity();
const lines = [
"/* " + t.label + " — Hollowbrook blueprint grid */",
".blueprint {",
" --g-fine: " + state.fine + "px;",
" --g-major: " + state.major + "px;",
" --g-op: " + state.opacity.toFixed(2) + ";",
" --g-op-major: " + mo + ";",
" --g-rgb: " + t.rgb + ";",
"",
" background-color: " + t.base2 + ";",
" background-image:",
" linear-gradient(to right, rgba(var(--g-rgb), var(--g-op-major)) 1px, transparent 1px),",
" linear-gradient(to bottom, rgba(var(--g-rgb), var(--g-op-major)) 1px, transparent 1px),",
" linear-gradient(to right, rgba(var(--g-rgb), var(--g-op)) 1px, transparent 1px),",
" linear-gradient(to bottom, rgba(var(--g-rgb), var(--g-op)) 1px, transparent 1px),",
" radial-gradient(120% 90% at 50% -10%, " + t.base + " 0%, " + t.base2 + " 72%);",
" background-size:",
" var(--g-major) var(--g-major), var(--g-major) var(--g-major),",
" var(--g-fine) var(--g-fine), var(--g-fine) var(--g-fine),",
" cover;",
"}"
];
if (state.grain) {
lines.push(
"",
".blueprint::after { /* paper grain */",
" content: \"\"; position: absolute; inset: 0; opacity: .5;",
" mix-blend-mode: overlay;",
" background-image:",
" radial-gradient(rgba(var(--g-rgb), .10) .5px, transparent .6px),",
" radial-gradient(rgba(var(--g-rgb), .07) .5px, transparent .6px);",
" background-size: 3px 3px, 7px 7px;",
"}"
);
}
if (state.vignette) {
lines.push(
"",
".blueprint::before { /* vignette */",
" content: \"\"; position: absolute; inset: 0;",
" background: radial-gradient(120% 85% at 50% 40%, transparent 40%, rgba(0,0,0,.5) 100%);",
"}"
);
}
if (state.drift) {
lines.push(
"",
"@media (prefers-reduced-motion: no-preference) {",
" .blueprint { animation: drift 40s linear infinite; }",
" @keyframes drift { to { background-position:",
" var(--g-major) var(--g-major), var(--g-major) var(--g-major),",
" var(--g-major) var(--g-major), var(--g-major) var(--g-major), 0 0; } }",
"}"
);
}
return lines.join("\n");
}
function renderCode() {
$("#codeOut").textContent = buildCSS();
}
/* ---------- wiring ---------- */
$$("[data-theme-btn]").forEach((btn) => {
btn.addEventListener("click", () => {
state.theme = btn.dataset.themeBtn;
apply();
toast(THEMES[state.theme].label + " applied");
});
btn.addEventListener("keydown", (e) => {
const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp"];
if (!keys.includes(e.key)) return;
e.preventDefault();
const btns = $$("[data-theme-btn]");
const i = btns.indexOf(btn);
const dir = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : -1;
const next = btns[(i + dir + btns.length) % btns.length];
next.focus();
next.click();
});
});
const bind = (id, key, parse) => {
const el = $(id);
el.addEventListener("input", () => {
state[key] = parse(el.value);
apply();
});
};
bind("#fine", "fine", (v) => parseInt(v, 10));
bind("#major", "major", (v) => parseInt(v, 10));
bind("#opacity", "opacity", (v) => parseFloat(v));
const bindToggle = (id, key, msg) => {
const el = $(id);
el.addEventListener("change", () => {
state[key] = el.checked;
apply();
toast(msg + (el.checked ? " on" : " off"));
});
};
bindToggle("#tVignette", "vignette", "Vignette");
bindToggle("#tGrain", "grain", "Paper grain");
bindToggle("#tDrift", "drift", "Drift");
function syncMotion() {
const drift = $("#tDrift");
const hint = $("#motionHint");
drift.disabled = reduceMotion.matches;
hint.hidden = !reduceMotion.matches;
apply();
}
if (reduceMotion.addEventListener) reduceMotion.addEventListener("change", syncMotion);
/* copy */
$("#copyBtn").addEventListener("click", async () => {
const css = buildCSS();
try {
await navigator.clipboard.writeText(css);
toast("CSS copied to clipboard");
} catch (err) {
const ta = document.createElement("textarea");
ta.value = css;
ta.setAttribute("readonly", "");
ta.style.cssText = "position:fixed;top:-1000px;opacity:0";
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
ta.remove();
toast(ok ? "CSS copied to clipboard" : "Copy blocked — select the code manually");
}
});
/* shuffle */
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
$("#randomize").addEventListener("click", () => {
state.theme = pick(Object.keys(THEMES));
state.fine = 4 + Math.floor(Math.random() * 13);
state.major = 40 + Math.floor(Math.random() * 16) * 8;
state.opacity = +(0.06 + Math.random() * 0.3).toFixed(2);
state.vignette = Math.random() > 0.35;
state.grain = Math.random() > 0.4;
$("#fine").value = state.fine;
$("#major").value = state.major;
$("#opacity").value = state.opacity;
$("#tVignette").checked = state.vignette;
$("#tGrain").checked = state.grain;
apply();
toast("Shuffled — " + THEMES[state.theme].label + " @ " + state.fine + "px");
});
/* smooth scroll buttons */
$$("[data-scroll]").forEach((b) => {
b.addEventListener("click", () => {
const target = $(b.dataset.scroll);
if (target) target.scrollIntoView({ behavior: reduceMotion.matches ? "auto" : "smooth", block: "start" });
});
});
syncMotion();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Blueprint Grid Background — Hollowbrook Workshop</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body data-theme="navy">
<div class="bp-canvas" aria-hidden="true">
<div class="bp-layer bp-grid"></div>
<div class="bp-layer bp-grain"></div>
<div class="bp-layer bp-vignette"></div>
</div>
<a class="skip" href="#controls">Skip to controls</a>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 32 32" role="presentation">
<rect x="3" y="3" width="26" height="26" rx="4" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M3 12h26M12 3v26" stroke="currentColor" stroke-width="1.2" opacity=".55"/>
<circle cx="21" cy="21" r="4.5" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</span>
<span class="brand-text">
<strong>Hollowbrook</strong>
<em>Workshop Drafting Kit</em>
</span>
</div>
<nav class="topnav" aria-label="Sections">
<a href="#hero">Overview</a>
<a href="#controls">Tuner</a>
<a href="#presets">Presets</a>
</nav>
<span class="rev">REV <span class="mono">04·B</span></span>
</header>
<main>
<!-- HERO -->
<section id="hero" class="hero">
<article class="hero-card">
<div class="hero-head">
<span class="chip chip-orange">Sheet 1 of 3</span>
<span class="chip">Drawn by <b>M. Aldridge</b></span>
<span class="chip mono">DWG-2291-A</span>
</div>
<h1>A blueprint grid made of nothing but gradients.</h1>
<p class="lede">
No tiled PNGs, no SVG sprites, no canvas. Four stacked
<code>linear-gradient</code> layers, two background sizes and a handful of custom
properties give you a drafting-table surface that scales to any viewport and
retints instantly.
</p>
<dl class="specs">
<div><dt>Fine grid</dt><dd class="mono" data-out="fine">8px</dd></div>
<div><dt>Major line</dt><dd class="mono" data-out="major">80px</dd></div>
<div><dt>Opacity</dt><dd class="mono" data-out="opacity">0.14</dd></div>
<div><dt>Payload</dt><dd class="mono">0 KB images</dd></div>
</dl>
<div class="hero-actions">
<button class="btn btn-primary" type="button" data-scroll="#controls">Open the tuner</button>
<button class="btn btn-ghost" type="button" id="randomize">Shuffle a look</button>
</div>
</article>
<aside class="hero-side" aria-label="Sheet notes">
<h2 class="side-title">Sheet notes</h2>
<ul class="notes">
<li><span class="mono">01</span> Grid origin is top-left of the viewport.</li>
<li><span class="mono">02</span> Major lines every <b data-out="major-multi">10</b> fine cells.</li>
<li><span class="mono">03</span> Drift pauses under reduced motion.</li>
<li><span class="mono">04</span> Grain layer is a repeating radial dot mask.</li>
</ul>
<div class="stamp" role="img" aria-label="Approved stamp">
<span>APPROVED</span>
<span class="mono">30·07·26</span>
</div>
</aside>
</section>
<!-- CONTROLS -->
<section id="controls" class="panel-wrap">
<div class="panel">
<header class="panel-head">
<h2>Grid tuner</h2>
<p>Every control writes a CSS custom property on <code>:root</code>.</p>
</header>
<div class="panel-body">
<div class="control-col">
<fieldset class="fld">
<legend>Theme</legend>
<div class="themes" role="radiogroup" aria-label="Grid theme">
<button class="theme" role="radio" aria-checked="true" data-theme-btn="navy">
<span class="sw sw-navy" aria-hidden="true"></span>
<span class="theme-name">Navy blueprint</span>
<span class="theme-sub mono">#0f2a4a</span>
</button>
<button class="theme" role="radio" aria-checked="false" data-theme-btn="kraft">
<span class="sw sw-kraft" aria-hidden="true"></span>
<span class="theme-name">Kraft paper</span>
<span class="theme-sub mono">#d9c7a7</span>
</button>
<button class="theme" role="radio" aria-checked="false" data-theme-btn="dark">
<span class="sw sw-dark" aria-hidden="true"></span>
<span class="theme-name">Dark shop</span>
<span class="theme-sub mono">#0b0d10</span>
</button>
<button class="theme" role="radio" aria-checked="false" data-theme-btn="cyan">
<span class="sw sw-cyan" aria-hidden="true"></span>
<span class="theme-name">Cyan-print</span>
<span class="theme-sub mono">#0a6d8f</span>
</button>
</div>
</fieldset>
<fieldset class="fld">
<legend>Geometry</legend>
<div class="row">
<label for="fine">Fine grid size</label>
<output class="mono" id="fineOut">8px</output>
</div>
<input id="fine" type="range" min="4" max="16" step="1" value="8" />
<div class="row">
<label for="major">Major line interval</label>
<output class="mono" id="majorOut">80px</output>
</div>
<input id="major" type="range" min="40" max="160" step="8" value="80" />
<div class="row">
<label for="opacity">Line opacity</label>
<output class="mono" id="opacityOut">0.14</output>
</div>
<input id="opacity" type="range" min="0.02" max="0.45" step="0.01" value="0.14" />
</fieldset>
<fieldset class="fld">
<legend>Overlays</legend>
<div class="toggles">
<label class="tgl"><input type="checkbox" id="tVignette" checked /><span class="tgl-ui" aria-hidden="true"></span><span>Vignette</span></label>
<label class="tgl"><input type="checkbox" id="tDrift" /><span class="tgl-ui" aria-hidden="true"></span><span>Drift animation</span></label>
<label class="tgl"><input type="checkbox" id="tGrain" checked /><span class="tgl-ui" aria-hidden="true"></span><span>Paper grain</span></label>
</div>
<p class="hint" id="motionHint" hidden>Your system requests reduced motion — drift stays paused.</p>
</fieldset>
</div>
<div class="code-col">
<div class="code-head">
<span class="mono">generated.css</span>
<button class="btn btn-copy" type="button" id="copyBtn">Copy CSS</button>
</div>
<pre class="code"><code id="codeOut"></code></pre>
<p class="code-foot">Paste into any element — the layers are self-contained.</p>
</div>
</div>
</div>
</section>
<!-- PRESETS -->
<section id="presets" class="presets">
<header class="sec-head">
<h2>Same system, three scales</h2>
<p>Each card runs the identical gradient stack with different variable values.</p>
</header>
<div class="preset-grid">
<article class="preset" data-preset="hero" style="--p-fine:10px;--p-major:100px;--p-op:0.22">
<div class="preset-fill"></div>
<div class="preset-meta">
<span class="badge">Hero band</span>
<h3>Full-bleed masthead</h3>
<p>Wide cells, strong majors — reads as architecture behind large type.</p>
<span class="mono tiny">10px / 100px / 0.22</span>
</div>
</article>
<article class="preset" data-preset="divider" style="--p-fine:6px;--p-major:48px;--p-op:0.16">
<div class="preset-fill"></div>
<div class="preset-meta">
<span class="badge">Section divider</span>
<h3>Tight rule strip</h3>
<p>Short height plus a fade mask makes a graph-paper separator.</p>
<span class="mono tiny">6px / 48px / 0.16</span>
</div>
</article>
<article class="preset" data-preset="surface" style="--p-fine:4px;--p-major:40px;--p-op:0.10">
<div class="preset-fill"></div>
<div class="preset-meta">
<span class="badge">Card surface</span>
<h3>Whisper texture</h3>
<p>Micro cells at low opacity — texture you feel more than see.</p>
<span class="mono tiny">4px / 40px / 0.10</span>
</div>
</article>
</div>
</section>
<section class="ledger">
<table>
<caption>Layer inventory — what each gradient contributes</caption>
<thead>
<tr><th scope="col">#</th><th scope="col">Layer</th><th scope="col">Role</th><th scope="col">Size</th></tr>
</thead>
<tbody>
<tr><td class="mono">01</td><td>Major vertical</td><td>Structural column rule</td><td class="mono" data-out="major">80px</td></tr>
<tr><td class="mono">02</td><td>Major horizontal</td><td>Structural row rule</td><td class="mono" data-out="major">80px</td></tr>
<tr><td class="mono">03</td><td>Fine vertical</td><td>Measurement ticks</td><td class="mono" data-out="fine">8px</td></tr>
<tr><td class="mono">04</td><td>Fine horizontal</td><td>Measurement ticks</td><td class="mono" data-out="fine">8px</td></tr>
<tr><td class="mono">05</td><td>Base wash</td><td>Theme tint under everything</td><td class="mono">cover</td></tr>
</tbody>
</table>
</section>
</main>
<footer class="foot">
<span>Hollowbrook Workshop · fictional drafting supply co.</span>
<span class="mono">SHEET 1/3 · SCALE 1:1</span>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Blueprint Grid Background
The page opens on a fixed, full-bleed drafting surface: a radial base wash for the paper tint, then four hairline gradients that draw fine measurement ticks and heavier structural rules on top of it. A sample hero sheet from the fictional Hollowbrook Workshop floats above it with a dashed inner border, revision chips and a rotated APPROVED stamp, so you can judge the texture against real type instead of an empty swatch.
The tuner drives everything live. Three sliders set the fine cell size (4–16px), the major line interval (40–160px) and the line opacity; the major lines derive their own alpha from that value so the hierarchy never inverts. A four-way theme switcher — navy blueprint, kraft paper, dark shop with faint cyan lines, and inverted cyan-print — retints the base, the line colour and the whole UI chrome at once, and is keyboard-navigable with arrow keys as a proper radio group. Toggles add or remove the vignette, the paper-grain overlay and a slow diagonal drift.
A code panel mirrors the current state as copy-ready CSS, updating on every input, with a clipboard button and a toast. Below it, three preset cards run the identical gradient stack at different scales — a wide hero band, a masked divider strip and a whisper-fine card surface — followed by a layer inventory table naming what each of the five gradients contributes.
How this look is built
The whole effect is four linear-gradient layers plus one radial-gradient wash sharing a single background-image, with a matching multi-value background-size so each pair of layers tiles at its own interval — the old approach was a repeating PNG per theme, which meant a new asset for every colour and a blurry result on high-DPI screens. Because the sizes and colours are var() references (--g-fine, --g-major, --g-op, --g-rgb), JS only ever calls style.setProperty on :root; no class swapping, no re-layout, and rgba(var(--g-rgb), var(--g-op)) lets one channel carry the theme while another carries the alpha. The divider preset uses mask-image to fade the grid out at both edges rather than stacking a matching-colour gradient over it. Watch the drift: animating background-position repaints a viewport-sized layer, so it is confined to one fixed, -20%-inset element behind everything, marked will-change, and its animation-play-state is driven by the same variable the toggle writes — under prefers-reduced-motion: reduce the checkbox is disabled and the keyframes are dropped entirely, and the reduced-motion query also disables smooth scrolling.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.