DIY — Graph Paper Background
An engineering graph-paper surface drawn entirely in CSS, with a three-tier grid of 1mm fine, 5mm medium and 10mm bold lines layered from gradients and driven by custom properties. A notebook-style build-log card sits on the sheet while a control panel switches scale presets, paper tint, a red margin rule, corner registration marks, a punched-holes strip, fibre grain and a 60-degree isometric variant. A live ruler redraws with the scale and a code panel emits copyable CSS.
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-sm: 0 1px 2px rgba(28, 39, 51, 0.06), 0 2px 6px rgba(28, 39, 51, 0.05);
--sh-md: 0 2px 6px rgba(28, 39, 51, 0.07), 0 12px 30px rgba(28, 39, 51, 0.1);
--sh-lg: 0 6px 18px rgba(28, 39, 51, 0.1), 0 28px 60px rgba(28, 39, 51, 0.14);
/* Graph-paper engine */
--unit: 4px; /* px per millimetre */
--fine: var(--unit);
--med: calc(var(--unit) * 5);
--bold: calc(var(--unit) * 10);
--ink-mul: 1;
--c-fine: rgba(43, 92, 150, calc(0.16 * var(--ink-mul)));
--c-med: rgba(43, 92, 150, calc(0.34 * var(--ink-mul)));
--c-bold: rgba(30, 70, 120, calc(0.62 * var(--ink-mul)));
--w-fine: 1px;
--w-med: 1px;
--w-bold: 1.6px;
--paper: #ffffff;
--paper-edge: rgba(28, 39, 51, 0.06);
--ruler-h: 30px;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
min-height: 100%;
}
body {
font-family: Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
:focus-visible {
outline: 3px solid var(--orange);
outline-offset: 2px;
border-radius: var(--r-sm);
}
/* ── Paper surface ───────────────────────────────────────────── */
.paper {
position: fixed;
inset: 0;
z-index: 0;
background: var(--paper);
transition: background-color 260ms ease;
}
.paper[data-tint="white"] { --paper: #ffffff; }
.paper[data-tint="cream"] { --paper: #f7f0e1; --c-fine: rgba(150, 110, 60, calc(0.18 * var(--ink-mul))); --c-med: rgba(150, 110, 60, calc(0.36 * var(--ink-mul))); --c-bold: rgba(126, 88, 44, calc(0.6 * var(--ink-mul))); }
.paper[data-tint="green"] { --paper: #e9f3e7; --c-fine: rgba(46, 108, 74, calc(0.18 * var(--ink-mul))); --c-med: rgba(46, 108, 74, calc(0.36 * var(--ink-mul))); --c-bold: rgba(32, 84, 56, calc(0.6 * var(--ink-mul))); }
.paper[data-tint="blue"] { --paper: #e7effa; }
/* Three-tier grid built from six linear-gradient layers */
.paper__grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, var(--c-bold) var(--w-bold), transparent var(--w-bold)),
linear-gradient(to bottom, var(--c-bold) var(--w-bold), transparent var(--w-bold)),
linear-gradient(to right, var(--c-med) var(--w-med), transparent var(--w-med)),
linear-gradient(to bottom, var(--c-med) var(--w-med), transparent var(--w-med)),
linear-gradient(to right, var(--c-fine) var(--w-fine), transparent var(--w-fine)),
linear-gradient(to bottom, var(--c-fine) var(--w-fine), transparent var(--w-fine));
background-size:
var(--bold) var(--bold),
var(--bold) var(--bold),
var(--med) var(--med),
var(--med) var(--med),
var(--fine) var(--fine),
var(--fine) var(--fine);
background-position: 0 0;
transition: background-size 260ms cubic-bezier(0.22, 0.7, 0.25, 1);
}
.paper.is-iso .paper__grid { opacity: 0; }
/* Isometric variant: 3 repeating-linear-gradient layers at 0/60/120 */
.paper__iso {
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 260ms ease;
background-image:
repeating-linear-gradient(0deg, var(--c-med) 0 var(--w-med), transparent var(--w-med) var(--med)),
repeating-linear-gradient(60deg, var(--c-fine) 0 var(--w-fine), transparent var(--w-fine) var(--med)),
repeating-linear-gradient(-60deg, var(--c-fine) 0 var(--w-fine), transparent var(--w-fine) var(--med));
}
.paper.is-iso .paper__iso { opacity: 1; }
/* Fibre grain */
.paper__grain {
position: absolute;
inset: 0;
opacity: 0;
mix-blend-mode: multiply;
transition: opacity 260ms ease;
background-image:
radial-gradient(circle at 20% 30%, rgba(28, 39, 51, 0.05) 0 1px, transparent 1.4px),
radial-gradient(circle at 70% 65%, rgba(28, 39, 51, 0.045) 0 1px, transparent 1.4px),
radial-gradient(circle at 45% 85%, rgba(28, 39, 51, 0.04) 0 1px, transparent 1.6px),
radial-gradient(circle at 88% 12%, rgba(28, 39, 51, 0.05) 0 1px, transparent 1.3px);
background-size: 37px 41px, 53px 47px, 61px 59px, 29px 31px;
}
.paper.is-grain .paper__grain { opacity: 1; }
/* Red margin rule */
.paper__margin {
position: absolute;
top: 0;
bottom: 0;
left: 44px;
width: 3px;
opacity: 0;
transition: opacity 220ms ease;
background: linear-gradient(to bottom, rgba(212, 80, 62, 0) 0, rgba(212, 80, 62, 0.55) 4%, rgba(212, 80, 62, 0.55) 96%, rgba(212, 80, 62, 0) 100%);
box-shadow: 6px 0 0 -5px rgba(212, 80, 62, 0.25);
}
.paper.is-margin .paper__margin { opacity: 1; }
/* Punched holes */
.paper__holes {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 44px;
display: none;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 4vh 0;
}
.paper.is-holes .paper__holes { display: flex; }
.paper__holes span {
width: 17px;
height: 17px;
border-radius: 50%;
background: var(--bg);
box-shadow:
inset 0 2px 3px rgba(28, 39, 51, 0.28),
inset 0 -1px 0 rgba(255, 255, 255, 0.7);
}
/* Registration marks */
.regmark {
position: absolute;
width: 34px;
height: 34px;
opacity: 0;
transition: opacity 220ms ease;
pointer-events: none;
}
.regmark line, .regmark circle {
stroke: rgba(28, 39, 51, 0.5);
stroke-width: 1.2;
fill: none;
}
.regmark circle { stroke-dasharray: 4 3.5; }
.paper.is-marks .regmark { opacity: 1; }
.regmark--tl { top: 14px; left: 14px; }
.regmark--tr { top: 14px; right: 14px; }
.regmark--bl { bottom: 14px; left: 14px; }
.regmark--br { bottom: 14px; right: 14px; }
/* ── Ruler strip ─────────────────────────────────────────────── */
.ruler {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--ruler-h);
z-index: 3;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.96), rgba(244, 241, 234, 0.92));
border-bottom: 1px solid var(--line-2);
backdrop-filter: blur(6px);
transition: transform 260ms ease, opacity 200ms ease;
}
.ruler.is-hidden {
transform: translateY(-100%);
opacity: 0;
pointer-events: none;
}
.ruler svg { display: block; width: 100%; height: var(--ruler-h); }
.ruler .tick { stroke: var(--ink-2); stroke-width: 1; }
.ruler .tick--major { stroke: var(--ink); stroke-width: 1.4; }
.ruler .tick--fine { stroke: rgba(61, 74, 87, 0.4); }
.ruler .num {
font-family: "JetBrains Mono", monospace;
font-size: 9px;
font-weight: 600;
fill: var(--ink-2);
}
/* ── Shell ───────────────────────────────────────────────────── */
.shell {
position: relative;
z-index: 2;
max-width: 1180px;
margin: 0 auto;
padding: calc(var(--ruler-h) + 30px) 22px 56px;
}
.masthead {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 22px;
flex-wrap: wrap;
}
.masthead__mark {
width: 46px;
height: 46px;
border-radius: var(--r-md);
background: var(--blueprint);
display: grid;
place-items: center;
box-shadow: var(--sh-md);
flex: none;
}
.masthead__mark svg { width: 30px; height: 30px; }
.masthead__mark rect, .masthead__mark line { fill: none; stroke: var(--orange); stroke-width: 2.2; }
.masthead__mark line { stroke: rgba(140, 180, 220, 0.55); stroke-width: 1.4; }
.masthead h1 {
margin: 2px 0 0;
font-size: clamp(1.35rem, 3.4vw, 1.95rem);
font-weight: 800;
letter-spacing: -0.02em;
}
.eyebrow {
margin: 0;
font-family: "JetBrains Mono", monospace;
font-size: 0.66rem;
font-weight: 600;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
}
.chip {
margin-left: auto;
font-family: "JetBrains Mono", monospace;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 6px 11px;
border-radius: 999px;
background: var(--white);
border: 1px solid var(--line-2);
color: var(--ink-2);
white-space: nowrap;
}
.chip--ok { background: rgba(47, 158, 111, 0.12); border-color: rgba(47, 158, 111, 0.35); color: #1d6d4b; }
.chip--warn { background: rgba(217, 138, 43, 0.13); border-color: rgba(217, 138, 43, 0.38); color: #8f5a12; margin-left: 0; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(310px, 0.95fr);
gap: 22px;
align-items: start;
}
/* ── Notebook card ───────────────────────────────────────────── */
.notebook {
position: relative;
background: rgba(255, 255, 255, 0.94);
border: 1px solid var(--line);
border-left: 4px solid var(--orange);
border-radius: var(--r-lg);
padding: 26px 26px 22px;
box-shadow: var(--sh-lg);
backdrop-filter: blur(2px);
}
.notebook__tape {
position: absolute;
top: -13px;
left: 50%;
transform: translateX(-50%) rotate(-1.4deg);
width: 122px;
height: 26px;
background: linear-gradient(180deg, rgba(217, 199, 167, 0.85), rgba(239, 230, 212, 0.7));
border-left: 1px dashed rgba(28, 39, 51, 0.18);
border-right: 1px dashed rgba(28, 39, 51, 0.18);
box-shadow: 0 2px 6px rgba(28, 39, 51, 0.12);
}
.notebook__head {
display: flex;
gap: 14px;
align-items: flex-start;
justify-content: space-between;
flex-wrap: wrap;
padding-bottom: 14px;
border-bottom: 1px dashed var(--line-2);
}
.notebook__head h2 {
margin: 4px 0 0;
font-size: 1.28rem;
font-weight: 700;
letter-spacing: -0.015em;
}
.lede {
margin: 16px 0 18px;
color: var(--ink-2);
font-size: 0.95rem;
max-width: 62ch;
}
.specs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.spec {
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 9px 12px;
background: linear-gradient(180deg, rgba(244, 241, 234, 0.75), rgba(255, 255, 255, 0.6));
}
.spec__k {
display: block;
font-family: "JetBrains Mono", monospace;
font-size: 0.6rem;
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--muted);
}
.spec__v {
display: block;
font-weight: 700;
font-size: 0.9rem;
margin-top: 3px;
}
.cuts {
width: 100%;
border-collapse: collapse;
font-size: 0.87rem;
}
.cuts th {
text-align: left;
font-family: "JetBrains Mono", monospace;
font-size: 0.62rem;
letter-spacing: 0.11em;
text-transform: uppercase;
color: var(--muted);
padding: 8px 10px;
border-bottom: 1.5px solid var(--line-2);
}
.cuts td {
padding: 10px;
border-bottom: 1px solid var(--line);
vertical-align: middle;
}
.cuts td:nth-child(2), .cuts td:nth-child(3) { font-family: "JetBrains Mono", monospace; }
.cuts tbody tr { transition: background-color 140ms ease; }
.cuts tbody tr:hover { background: rgba(255, 107, 53, 0.07); }
.cuts b { font-family: "JetBrains Mono", monospace; font-weight: 700; color: var(--blueprint); }
.pill {
display: inline-block;
font-family: "JetBrains Mono", monospace;
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
background: rgba(107, 120, 133, 0.14);
color: var(--ink-2);
white-space: nowrap;
}
.pill--ok { background: rgba(47, 158, 111, 0.16); color: #1d6d4b; }
.pill--warn { background: rgba(217, 138, 43, 0.18); color: #8f5a12; }
.pill--danger { background: rgba(212, 80, 62, 0.16); color: #9c3628; }
.note {
margin: 18px 0 0;
padding: 12px 14px;
border-left: 3px solid var(--orange);
background: var(--orange-soft);
border-radius: 0 var(--r-md) var(--r-md) 0;
font-size: 0.88rem;
color: #6d3313;
}
/* ── Panel ───────────────────────────────────────────────────── */
.panel {
position: sticky;
top: calc(var(--ruler-h) + 16px);
background: rgba(255, 255, 255, 0.95);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--sh-md);
backdrop-filter: blur(2px);
}
.panel__title {
margin: 0 0 16px;
font-size: 0.95rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.group {
border: 0;
border-top: 1px dashed var(--line-2);
margin: 0 0 16px;
padding: 14px 0 0;
}
.group:first-of-type { border-top: 0; padding-top: 0; }
.group legend {
font-family: "JetBrains Mono", monospace;
font-size: 0.6rem;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
padding: 0;
margin-bottom: 9px;
}
.segmented {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.segmented button {
font: inherit;
font-family: "JetBrains Mono", monospace;
font-size: 0.76rem;
font-weight: 600;
padding: 8px 4px;
border: 0;
border-radius: var(--r-sm);
background: transparent;
color: var(--ink-2);
cursor: pointer;
transition: background-color 160ms ease, color 160ms ease, transform 100ms ease;
}
.segmented button:hover { background: rgba(28, 39, 51, 0.06); }
.segmented button:active { transform: translateY(1px); }
.segmented button[aria-checked="true"] {
background: var(--blueprint);
color: #fff;
box-shadow: var(--sh-sm);
}
.hint { margin: 9px 0 0; font-size: 0.76rem; color: var(--muted); }
.swatches {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.swatches button {
font: inherit;
display: grid;
gap: 5px;
justify-items: center;
padding: 7px 3px;
border: 1.5px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
cursor: pointer;
transition: border-color 160ms ease, transform 120ms ease, box-shadow 160ms ease;
}
.swatches button:hover { transform: translateY(-2px); box-shadow: var(--sh-sm); }
.swatches button[aria-checked="true"] { border-color: var(--orange); box-shadow: 0 0 0 3px rgba(255, 107, 53, 0.16); }
.swatches i {
width: 26px; height: 26px; border-radius: var(--r-sm);
border: 1px solid var(--line-2); display: block;
}
.swatches span { font-size: 0.66rem; color: var(--ink-2); font-weight: 600; }
.toggles { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.tg {
display: flex;
gap: 11px;
align-items: flex-start;
padding: 9px 10px;
border-radius: var(--r-md);
cursor: pointer;
transition: background-color 140ms ease;
}
.tg:hover { background: rgba(28, 39, 51, 0.045); }
.tg input { position: absolute; opacity: 0; width: 0; height: 0; }
.tg__box {
flex: none;
margin-top: 2px;
width: 34px;
height: 19px;
border-radius: 999px;
background: rgba(28, 39, 51, 0.18);
position: relative;
transition: background-color 180ms ease;
}
.tg__box::after {
content: "";
position: absolute;
top: 2px; left: 2px;
width: 15px; height: 15px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-sm);
transition: transform 180ms cubic-bezier(0.22, 0.7, 0.25, 1);
}
.tg input:checked + .tg__box { background: var(--orange); }
.tg input:checked + .tg__box::after { transform: translateX(15px); }
.tg input:focus-visible + .tg__box { outline: 3px solid var(--orange); outline-offset: 2px; }
.tg__t { font-size: 0.85rem; font-weight: 600; display: block; }
.tg__t small { display: block; font-weight: 400; font-size: 0.72rem; color: var(--muted); margin-top: 1px; }
.slider { display: block; margin-bottom: 12px; }
.slider > span {
display: flex; justify-content: space-between; align-items: baseline;
font-size: 0.79rem; color: var(--ink-2); margin-bottom: 6px;
}
.slider b { font-size: 0.74rem; color: var(--blueprint); }
input[type="range"] {
width: 100%;
accent-color: var(--orange);
cursor: pointer;
}
.output {
border: 1px solid var(--line-2);
border-radius: var(--r-md);
overflow: hidden;
margin-bottom: 12px;
}
.output__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 7px 8px 7px 11px;
background: var(--bg);
border-bottom: 1px solid var(--line);
font-size: 0.68rem;
color: var(--muted);
}
.output__code {
margin: 0;
padding: 12px;
background: var(--blueprint-d);
color: #cfe0f0;
font-size: 0.7rem;
line-height: 1.65;
max-height: 220px;
overflow: auto;
white-space: pre;
}
.output__code::-webkit-scrollbar { height: 8px; width: 8px; }
.output__code::-webkit-scrollbar-thumb { background: var(--blueprint-l); border-radius: 999px; }
.btn {
font: inherit;
font-size: 0.78rem;
font-weight: 700;
padding: 7px 13px;
border: 0;
border-radius: var(--r-sm);
background: var(--orange);
color: #fff;
cursor: pointer;
transition: background-color 150ms ease, transform 100ms ease;
}
.btn:hover { background: var(--orange-d); }
.btn:active { transform: translateY(1px); }
.btn--ghost {
width: 100%;
background: transparent;
color: var(--ink-2);
border: 1px solid var(--line-2);
padding: 10px;
}
.btn--ghost:hover { background: rgba(28, 39, 51, 0.05); color: var(--ink); }
.foot {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 26px;
padding-top: 14px;
border-top: 1px dashed var(--line-2);
font-size: 0.74rem;
color: var(--muted);
}
/* ── Toast ───────────────────────────────────────────────────── */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 18px);
z-index: 40;
background: var(--blueprint);
color: #eaf2fb;
font-size: 0.8rem;
font-weight: 600;
padding: 10px 18px;
border-radius: 999px;
box-shadow: var(--sh-lg);
opacity: 0;
pointer-events: none;
transition: opacity 200ms ease, transform 200ms ease;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
/* ── Responsive ──────────────────────────────────────────────── */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.panel { position: static; }
}
@media (max-width: 520px) {
.shell { padding: calc(var(--ruler-h) + 20px) 14px 40px; }
.notebook { padding: 22px 16px 18px; border-radius: var(--r-md); }
.panel { padding: 16px; border-radius: var(--r-md); }
.masthead { gap: 12px; }
.masthead h1 { font-size: 1.2rem; }
.chip { margin-left: 0; }
.swatches { grid-template-columns: repeat(2, 1fr); }
.cuts { font-size: 0.79rem; }
.cuts th:nth-child(2), .cuts td:nth-child(2) { display: none; }
.cuts td, .cuts th { padding: 8px 6px; }
.paper__holes, .paper__margin { left: 0; }
.regmark { width: 24px; height: 24px; }
.notebook__tape { width: 96px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}/* DIY — Graph Paper Background
Vanilla JS. Drives CSS custom properties on the paper layer,
redraws an SVG ruler, and emits copyable CSS. */
(function () {
"use strict";
var paper = document.getElementById("paper");
var root = document.documentElement;
var toastEl = document.getElementById("toast");
var rulerEl = document.getElementById("ruler");
var rulerSvg = document.getElementById("rulerSvg");
var cssOut = document.getElementById("cssOut");
var SVG_NS = "http://www.w3.org/2000/svg";
var DEFAULTS = {
scale: 5,
tint: "white",
unit: 4,
ink: 100,
margin: true,
marks: true,
holes: false,
grain: true,
iso: false,
ruler: true
};
var state = Object.assign({}, DEFAULTS);
var SCALE_HINTS = {
1: "Fine 1 mm lines dominate — dense millimetre paper.",
5: "Medium 5 mm lines dominate; fine 1 mm stays visible.",
10: "Bold 10 cm-style lines dominate — sketching weight."
};
var SCALE_LABEL = {
1: "1 mm dominant",
5: "5 mm dominant",
10: "10 mm dominant"
};
/* Per-preset ink multipliers + stroke widths for the three tiers */
var SCALE_TUNE = {
1: { fine: 0.34, med: 0.30, bold: 0.42, wFine: 1, wMed: 1, wBold: 1.2 },
5: { fine: 0.16, med: 0.34, bold: 0.62, wFine: 1, wMed: 1, wBold: 1.6 },
10: { fine: 0.07, med: 0.16, bold: 0.72, wFine: 1, wMed: 1, wBold: 2.2 }
};
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-on");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2000);
}
function inkColors() {
var t = SCALE_TUNE[state.scale];
var m = state.ink / 100;
var base = { white: "43, 92, 150", blue: "43, 92, 150", cream: "150, 110, 60", green: "46, 108, 74" }[state.tint];
var boldBase = { white: "30, 70, 120", blue: "30, 70, 120", cream: "126, 88, 44", green: "32, 84, 56" }[state.tint];
return {
fine: "rgba(" + base + ", " + (t.fine * m).toFixed(3) + ")",
med: "rgba(" + base + ", " + (t.med * m).toFixed(3) + ")",
bold: "rgba(" + boldBase + ", " + (t.bold * m).toFixed(3) + ")",
wFine: t.wFine + "px",
wMed: t.wMed + "px",
wBold: t.wBold + "px"
};
}
function apply() {
var c = inkColors();
paper.dataset.tint = state.tint;
paper.dataset.scale = String(state.scale);
paper.style.setProperty("--unit", state.unit + "px");
paper.style.setProperty("--c-fine", c.fine);
paper.style.setProperty("--c-med", c.med);
paper.style.setProperty("--c-bold", c.bold);
paper.style.setProperty("--w-fine", c.wFine);
paper.style.setProperty("--w-med", c.wMed);
paper.style.setProperty("--w-bold", c.wBold);
paper.classList.toggle("is-margin", state.margin);
paper.classList.toggle("is-marks", state.marks);
paper.classList.toggle("is-holes", state.holes);
paper.classList.toggle("is-grain", state.grain);
paper.classList.toggle("is-iso", state.iso);
rulerEl.classList.toggle("is-hidden", !state.ruler);
root.style.setProperty("--ruler-h", state.ruler ? "30px" : "0px");
document.getElementById("specScale").innerHTML =
state.iso ? "Isometric 60°" : SCALE_LABEL[state.scale];
document.getElementById("scaleHint").innerHTML = SCALE_HINTS[state.scale];
document.getElementById("unitVal").textContent = state.unit + " px / mm";
document.getElementById("inkVal").textContent = state.ink + "%";
syncControls();
drawRuler();
writeCss(c);
}
function syncControls() {
Array.prototype.forEach.call(
document.querySelectorAll("#scaleGroup button"),
function (b) { b.setAttribute("aria-checked", String(Number(b.dataset.scale) === state.scale)); }
);
Array.prototype.forEach.call(
document.querySelectorAll("#tintGroup button"),
function (b) { b.setAttribute("aria-checked", String(b.dataset.tint === state.tint)); }
);
document.getElementById("tMargin").checked = state.margin;
document.getElementById("tMarks").checked = state.marks;
document.getElementById("tHoles").checked = state.holes;
document.getElementById("tGrain").checked = state.grain;
document.getElementById("tIso").checked = state.iso;
document.getElementById("tRuler").checked = state.ruler;
document.getElementById("unit").value = String(state.unit);
document.getElementById("ink").value = String(state.ink);
}
/* ── Ruler ─────────────────────────────────────────────────── */
function drawRuler() {
if (!state.ruler) return;
var w = Math.max(320, rulerEl.clientWidth);
var h = 30;
rulerSvg.setAttribute("viewBox", "0 0 " + w + " " + h);
while (rulerSvg.firstChild) rulerSvg.removeChild(rulerSvg.firstChild);
var u = state.unit; // px per mm
var step = state.scale === 1 ? u : u; // fine tick every mm
var major = u * 10; // bold every 10 mm
var medium = u * 5;
// Skip fine ticks if they'd be denser than 3px
var drawFine = step >= 3;
for (var x = 0; x <= w; x += step) {
var isMajor = Math.abs((x / major) - Math.round(x / major)) < 0.001;
var isMed = Math.abs((x / medium) - Math.round(x / medium)) < 0.001;
if (!isMajor && !isMed && !drawFine) continue;
var line = document.createElementNS(SVG_NS, "line");
var len = isMajor ? 13 : isMed ? 8 : 4;
line.setAttribute("x1", x.toFixed(2));
line.setAttribute("x2", x.toFixed(2));
line.setAttribute("y1", String(h));
line.setAttribute("y2", String(h - len));
line.setAttribute("class", isMajor ? "tick tick--major" : isMed ? "tick" : "tick tick--fine");
rulerSvg.appendChild(line);
if (isMajor && major >= 26) {
var mm = Math.round(x / u);
var txt = document.createElementNS(SVG_NS, "text");
txt.setAttribute("x", (x + 3).toFixed(2));
txt.setAttribute("y", "11");
txt.setAttribute("class", "num");
txt.textContent = String(mm);
rulerSvg.appendChild(txt);
}
}
var base = document.createElementNS(SVG_NS, "line");
base.setAttribute("x1", "0");
base.setAttribute("x2", String(w));
base.setAttribute("y1", String(h - 0.5));
base.setAttribute("y2", String(h - 0.5));
base.setAttribute("class", "tick");
rulerSvg.appendChild(base);
}
/* ── CSS output ────────────────────────────────────────────── */
function writeCss(c) {
var u = state.unit;
var lines;
if (state.iso) {
lines = [
".paper {",
" --unit: " + u + "px;",
" --med: calc(var(--unit) * 5);",
" background-color: " + paperHex() + ";",
" background-image:",
" repeating-linear-gradient(0deg, " + c.med + " 0 " + c.wMed + ", transparent " + c.wMed + " var(--med)),",
" repeating-linear-gradient(60deg, " + c.fine + " 0 " + c.wFine + ", transparent " + c.wFine + " var(--med)),",
" repeating-linear-gradient(-60deg, " + c.fine + " 0 " + c.wFine + ", transparent " + c.wFine + " var(--med));",
"}"
];
} else {
lines = [
".paper {",
" --unit: " + u + "px; /* px per mm */",
" --fine: var(--unit);",
" --med: calc(var(--unit) * 5);",
" --bold: calc(var(--unit) * 10);",
" background-color: " + paperHex() + ";",
" background-image:",
" linear-gradient(to right, " + c.bold + " " + c.wBold + ", transparent " + c.wBold + "),",
" linear-gradient(to bottom, " + c.bold + " " + c.wBold + ", transparent " + c.wBold + "),",
" linear-gradient(to right, " + c.med + " " + c.wMed + ", transparent " + c.wMed + "),",
" linear-gradient(to bottom, " + c.med + " " + c.wMed + ", transparent " + c.wMed + "),",
" linear-gradient(to right, " + c.fine + " " + c.wFine + ", transparent " + c.wFine + "),",
" linear-gradient(to bottom, " + c.fine + " " + c.wFine + ", transparent " + c.wFine + ");",
" background-size:",
" var(--bold) var(--bold), var(--bold) var(--bold),",
" var(--med) var(--med), var(--med) var(--med),",
" var(--fine) var(--fine), var(--fine) var(--fine);",
"}"
];
}
if (state.margin) {
lines.push("");
lines.push(".paper::before { /* red margin rule */");
lines.push(" content: \"\"; position: absolute; inset: 0 auto 0 44px;");
lines.push(" width: 3px; background: rgba(212, 80, 62, 0.55);");
lines.push("}");
}
cssOut.textContent = lines.join("\n");
}
function paperHex() {
return { white: "#ffffff", cream: "#f7f0e1", green: "#e9f3e7", blue: "#e7effa" }[state.tint];
}
/* ── Events ────────────────────────────────────────────────── */
function wireRadioGroup(id, key, cast) {
var group = document.getElementById(id);
var buttons = Array.prototype.slice.call(group.querySelectorAll("button"));
group.addEventListener("click", function (e) {
var btn = e.target.closest("button");
if (!btn) return;
state[key] = cast(btn.dataset[key]);
apply();
});
group.addEventListener("keydown", function (e) {
var i = buttons.indexOf(document.activeElement);
if (i < 0) return;
var next = null;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = buttons[(i + 1) % buttons.length];
if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = buttons[(i - 1 + buttons.length) % buttons.length];
if (!next) return;
e.preventDefault();
next.focus();
next.click();
});
}
wireRadioGroup("scaleGroup", "scale", Number);
wireRadioGroup("tintGroup", "tint", String);
[["tMargin", "margin"], ["tMarks", "marks"], ["tHoles", "holes"],
["tGrain", "grain"], ["tIso", "iso"], ["tRuler", "ruler"]].forEach(function (pair) {
document.getElementById(pair[0]).addEventListener("change", function (e) {
state[pair[1]] = e.target.checked;
apply();
if (pair[1] === "iso") toast(e.target.checked ? "Isometric 60° grid on" : "Back to square grid");
});
});
document.getElementById("unit").addEventListener("input", function (e) {
state.unit = Number(e.target.value);
apply();
});
document.getElementById("ink").addEventListener("input", function (e) {
state.ink = Number(e.target.value);
apply();
});
document.getElementById("copyBtn").addEventListener("click", function () {
var text = cssOut.textContent;
var done = function () { toast("CSS copied to clipboard"); };
var fail = function () {
try {
var ta = document.createElement("textarea");
ta.value = text;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
done();
} catch (err) {
toast("Copy failed — select the code manually");
}
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, fail);
} else {
fail();
}
});
document.getElementById("resetBtn").addEventListener("click", function () {
state = Object.assign({}, DEFAULTS);
apply();
toast("Sheet reset to defaults");
});
var raf = 0;
window.addEventListener("resize", function () {
window.cancelAnimationFrame(raf);
raf = window.requestAnimationFrame(drawRuler);
});
apply();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DIY — Graph Paper Background</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>
<div class="paper" id="paper" data-tint="white" data-scale="5" aria-hidden="false">
<div class="paper__grid" id="paperGrid"></div>
<div class="paper__iso" id="paperIso"></div>
<div class="paper__grain" id="paperGrain"></div>
<div class="paper__margin" id="paperMargin"></div>
<div class="paper__holes" id="paperHoles" role="presentation">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<svg class="regmark regmark--tl" viewBox="0 0 40 40" aria-hidden="true"><g><line x1="20" y1="4" x2="20" y2="36"/><line x1="4" y1="20" x2="36" y2="20"/><circle cx="20" cy="20" r="9"/></g></svg>
<svg class="regmark regmark--tr" viewBox="0 0 40 40" aria-hidden="true"><g><line x1="20" y1="4" x2="20" y2="36"/><line x1="4" y1="20" x2="36" y2="20"/><circle cx="20" cy="20" r="9"/></g></svg>
<svg class="regmark regmark--bl" viewBox="0 0 40 40" aria-hidden="true"><g><line x1="20" y1="4" x2="20" y2="36"/><line x1="4" y1="20" x2="36" y2="20"/><circle cx="20" cy="20" r="9"/></g></svg>
<svg class="regmark regmark--br" viewBox="0 0 40 40" aria-hidden="true"><g><line x1="20" y1="4" x2="20" y2="36"/><line x1="4" y1="20" x2="36" y2="20"/><circle cx="20" cy="20" r="9"/></g></svg>
</div>
<div class="ruler" id="ruler" aria-hidden="true">
<svg id="rulerSvg" preserveAspectRatio="none" role="presentation"></svg>
</div>
<main class="shell">
<header class="masthead">
<div class="masthead__mark" aria-hidden="true">
<svg viewBox="0 0 48 48"><rect x="6" y="6" width="36" height="36" rx="4"/><line x1="6" y1="18" x2="42" y2="18"/><line x1="6" y1="30" x2="42" y2="30"/><line x1="18" y1="6" x2="18" y2="42"/><line x1="30" y1="6" x2="30" y2="42"/></svg>
</div>
<div>
<p class="eyebrow">Fenwick & Roe · Drafting Supplies</p>
<h1>Graph Paper Background</h1>
</div>
<span class="chip chip--ok">Pure CSS surface</span>
</header>
<div class="layout">
<!-- Notebook content card -->
<section class="notebook" aria-labelledby="nbTitle">
<div class="notebook__tape" aria-hidden="true"></div>
<div class="notebook__head">
<div>
<p class="eyebrow">Build log · Sheet 04 of 12</p>
<h2 id="nbTitle">Fold-Down Workbench — Leg Assembly</h2>
</div>
<span class="chip chip--warn">Draft rev. C</span>
</div>
<p class="lede">Cut list checked against the offcut bin on 12 March. All stock is 18 mm birch ply except the pivot blocks, which are hard maple to take the repeated bolt load.</p>
<div class="specs">
<div class="spec"><span class="spec__k">Sheet scale</span><span class="spec__v" id="specScale">5 mm dominant</span></div>
<div class="spec"><span class="spec__k">Drawing no.</span><span class="spec__v">FR-2214-C</span></div>
<div class="spec"><span class="spec__k">Drawn by</span><span class="spec__v">M. Okonkwo</span></div>
<div class="spec"><span class="spec__k">Tolerance</span><span class="spec__v">±0.5 mm</span></div>
</div>
<table class="cuts">
<caption class="sr-only">Cut list for the leg assembly</caption>
<thead>
<tr><th scope="col">Part</th><th scope="col">Qty</th><th scope="col">Size (mm)</th><th scope="col">Status</th></tr>
</thead>
<tbody>
<tr><td><b>LG-01</b> Front leg</td><td>2</td><td>720 × 74 × 18</td><td><span class="pill pill--ok">Cut</span></td></tr>
<tr><td><b>LG-02</b> Rear leg</td><td>2</td><td>720 × 74 × 18</td><td><span class="pill pill--ok">Cut</span></td></tr>
<tr><td><b>PV-04</b> Pivot block</td><td>4</td><td>90 × 60 × 32</td><td><span class="pill pill--warn">On order</span></td></tr>
<tr><td><b>ST-07</b> Stretcher</td><td>2</td><td>540 × 90 × 18</td><td><span class="pill">Queued</span></td></tr>
<tr><td><b>HW-11</b> M8 carriage bolt</td><td>8</td><td>70 long</td><td><span class="pill pill--danger">Short by 2</span></td></tr>
</tbody>
</table>
<p class="note"><b>Note —</b> drill the pivot holes <em>before</em> tapering the legs; the untapered edge gives the fence something square to ride against.</p>
</section>
<!-- Control panel -->
<aside class="panel" aria-label="Graph paper controls">
<h2 class="panel__title">Paper controls</h2>
<fieldset class="group">
<legend>Scale preset</legend>
<div class="segmented" role="radiogroup" aria-label="Scale preset" id="scaleGroup">
<button type="button" role="radio" aria-checked="false" data-scale="1">1 mm</button>
<button type="button" role="radio" aria-checked="true" data-scale="5">5 mm</button>
<button type="button" role="radio" aria-checked="false" data-scale="10">10 mm</button>
</div>
<p class="hint" id="scaleHint">Medium 5 mm lines dominate; fine 1 mm stays visible.</p>
</fieldset>
<fieldset class="group">
<legend>Paper tint</legend>
<div class="swatches" role="radiogroup" aria-label="Paper tint" id="tintGroup">
<button type="button" role="radio" aria-checked="true" data-tint="white" title="Bright white"><i style="background:#ffffff"></i><span>White</span></button>
<button type="button" role="radio" aria-checked="false" data-tint="cream" title="Cream"><i style="background:#f6efe0"></i><span>Cream</span></button>
<button type="button" role="radio" aria-checked="false" data-tint="green" title="Pale engineering green"><i style="background:#e8f2e6"></i><span>Green</span></button>
<button type="button" role="radio" aria-checked="false" data-tint="blue" title="Pale blue"><i style="background:#e6eef7"></i><span>Blue</span></button>
</div>
</fieldset>
<fieldset class="group">
<legend>Sheet features</legend>
<ul class="toggles">
<li><label class="tg"><input type="checkbox" id="tMargin" checked /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Red margin rule<small>Left-hand vertical rule at 32 px</small></span></label></li>
<li><label class="tg"><input type="checkbox" id="tMarks" checked /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Registration marks<small>SVG crosshairs at four corners</small></span></label></li>
<li><label class="tg"><input type="checkbox" id="tHoles" /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Punched holes<small>Seven-hole filing strip</small></span></label></li>
<li><label class="tg"><input type="checkbox" id="tGrain" checked /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Paper fibre grain<small>Layered radial speckle</small></span></label></li>
<li><label class="tg"><input type="checkbox" id="tIso" /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Isometric variant<small>60° triangular grid</small></span></label></li>
<li><label class="tg"><input type="checkbox" id="tRuler" checked /><span class="tg__box" aria-hidden="true"></span><span class="tg__t">Top ruler strip<small>Ticks + mono numerals</small></span></label></li>
</ul>
</fieldset>
<fieldset class="group">
<legend>Grid density</legend>
<label class="slider">
<span>Base unit <b id="unitVal" class="mono">4 px / mm</b></span>
<input type="range" id="unit" min="2" max="8" step="0.5" value="4" />
</label>
<label class="slider">
<span>Line strength <b id="inkVal" class="mono">100%</b></span>
<input type="range" id="ink" min="30" max="160" step="5" value="100" />
</label>
</fieldset>
<div class="output">
<div class="output__bar">
<span class="mono">generated.css</span>
<button type="button" class="btn" id="copyBtn">Copy CSS</button>
</div>
<pre class="output__code mono" id="cssOut" tabindex="0"></pre>
</div>
<button type="button" class="btn btn--ghost" id="resetBtn">Reset to defaults</button>
</aside>
</div>
<footer class="foot">
<span class="mono">FR-2214-C</span>
<span>Fictional demo data · Fenwick & Roe is not a real company</span>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Graph Paper Background
The full-bleed surface behind the page is a real sheet of engineering paper: fine millimetre lines, a medium rule every 5 mm and a heavier one every 10 mm, all painted by a single fixed layer with no images and no canvas. Sitting on it is a notebook card from a fictional workshop, Fenwick & Roe — a build log for a fold-down workbench with a cut list, part numbers in mono, and status pills that show which stock is cut, on order or short.
The control panel on the right drives the sheet. Scale presets shift which tier dominates: at 1 mm the fine lines carry the weight, at 10 mm they nearly vanish and the bold rule takes over. Four tints — white, cream, pale engineering green and pale blue — retune both the paper colour and the ink hue so green paper gets green rules rather than blue ones. Independent toggles add the red left margin rule, dashed SVG registration crosshairs at the four corners, a seven-hole filing strip, a subtle fibre speckle, and a full isometric variant that swaps the square grid for a 60 degree triangular one.
Two sliders expose the underlying maths directly: base unit in pixels-per-millimetre, and an overall line-strength multiplier. Along the top edge a ruler strip redraws its ticks and mono numerals whenever the unit changes, dropping fine ticks automatically once they would sit closer than three pixels apart. The output panel below mirrors whatever you have built as copyable CSS.
How this look is built
The whole surface is six linear-gradient layers on one element — a hard colour stop at the line width followed by transparent, tiled by a matching background-size so each layer repeats at its own pitch, with --fine, --med and --bold all derived with calc() from a single --unit custom property. Because the tiers are pure background layers, changing scale or tint is one property write instead of a repaint of hundreds of DOM nodes, which is why this beats the old approach of absolutely positioned 1px divs or a tiled PNG that goes soft on retina. The isometric variant uses three repeating-linear-gradient layers at 0°, 60° and −60° and cross-fades by opacity so the square grid never flashes. The ruler is redrawn as SVG lines inside a requestAnimationFrame-throttled resize handler, and every transition is cancelled under prefers-reduced-motion; the one thing to watch is the fibre-grain layer, which uses mix-blend-mode: multiply and is worth disabling on very large viewports if you also animate over it.
Illustrative UI only — always follow real safety guidance when working with tools, electronics, or repairs.