Live route boards
Every route, ETA and bay on one screen. Late loads rise to the top while there is still time to act.
homepage · html, react
Copy this page into your project. Recolour it with tokens. It is yours from that moment.
One live board for every route, driver and bay, so late loads are handled before a customer has to ring.
Running depots across the north
The product
Northline replaces the whiteboard, the group chat and the status phone call with one board every shift can trust.
Every route, ETA and bay on one screen. Late loads rise to the top while there is still time to act.
Notes, photos and signatures travel with the job, so the evening shift starts where the morning left off.
Bay delays and capacity risks are flagged ahead of the afternoon rush, with the next free slot suggested.
One shared status
A change lands on the board once. The depot office, the driver app and the customer link all read it from there.
The board row updates the moment a driver flags a delay.
The handoff, notes and photos stay attached to the route.
The customer is told once, with a new arrival time.
We stopped chasing status calls. Dispatch, drivers and customers all read the same board.
Start the trial today and invite the whole ops team. Your own routes can be live within a day.
Paste into a static page. Link @bwui/css (and @bwui/js for behaviours).
<!DOCTYPE html>
<html lang="en-GB" data-bw-brand="northline">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northline: keep every route on time</title>
<meta
name="description"
content="Northline puts every route, driver and bay on one live board, so late loads are handled before a customer has to ring."
/>
<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=Source+Sans+3:wght@400;500;600;700&family=Source+Serif+4:opsz,wght@8..60,500;8..60,650;8..60,700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="/static/product_templates/assets/brickwork.css" />
<script defer src="/static/product_templates/assets/behaviours.js"></script>
<style>
/*
* Northline brand pack (fictional): cool paper, deep teal accent, display serif.
* Every colour is a --bw-* token or a tone derived from --bw-color-accent, so an
* inline accent override on the body recolours hero, board, depth band and close.
* The pack lives on the body because the kit's theme blocks set the same tokens
* on the element that carries the theme attribute; :root would lose to them.
* Literals are limited to neutral black shadows and mask alphas.
*/
:root {
--bw-font-family-sans: "Source Sans 3", system-ui, sans-serif;
--bw-font-family-display: "Source Serif 4", Georgia, serif;
}
html { scroll-padding-top: 5rem; }
@media (prefers-reduced-motion: no-preference) {
html { scroll-behavior: smooth; }
}
body:not([data-theme=dark]) {
--bw-color-surface: oklch(0.985 0.004 240);
--bw-color-fg: oklch(0.26 0.02 250);
--bw-color-border: oklch(0.90 0.01 240);
--bw-color-accent: oklch(0.40 0.06 228);
--bw-color-fg-on-accent: oklch(0.99 0 0);
--bw-color-success: oklch(0.56 0.13 155);
--bw-color-warning: oklch(0.70 0.14 75);
--bw-color-danger: oklch(0.55 0.18 25);
}
body[data-theme=dark] {
--bw-color-surface: oklch(0.17 0.012 240);
--bw-color-fg: oklch(0.94 0.006 240);
--bw-color-border: oklch(0.30 0.015 240);
--bw-color-accent: oklch(0.74 0.08 215);
--bw-color-fg-on-accent: oklch(0.18 0.02 240);
--bw-color-success: oklch(0.72 0.14 155);
--bw-color-warning: oklch(0.78 0.14 75);
--bw-color-danger: oklch(0.68 0.17 25);
}
/* Derived tones: hue and chroma follow the accent, lightness is fixed. */
body {
--nl-ink: oklch(from var(--bw-color-accent) 0.22 calc(min(c, 0.05)) h);
--nl-deep: oklch(from var(--bw-color-accent) 0.33 calc(min(c, 0.06)) h);
--nl-mist: oklch(from var(--bw-color-accent) 0.43 calc(min(c, 0.07)) h);
--nl-glow: oklch(from var(--bw-color-accent) 0.78 calc(min(c, 0.07)) h);
--nl-paper: oklch(from var(--bw-color-accent) 0.97 calc(min(c, 0.01)) h);
--nl-p72: color-mix(in oklab, var(--nl-paper) 72%, transparent);
--nl-p60: color-mix(in oklab, var(--nl-paper) 60%, transparent);
--nl-p16: color-mix(in oklab, var(--nl-paper) 16%, transparent);
--nl-p08: color-mix(in oklab, var(--nl-paper) 8%, transparent);
--nl-tint: color-mix(in oklab, var(--bw-color-accent) 6%, var(--bw-color-surface));
--nl-tint-2: color-mix(in oklab, var(--bw-color-accent) 12%, var(--bw-color-surface));
--nl-gutter: clamp(1.25rem, 4vw, 3rem);
--nl-edge: max(var(--nl-gutter), calc((100vw - 76rem) / 2));
--nl-y: clamp(4rem, 9vw, 7rem);
--nl-header-h: 4.25rem;
margin: 0;
font-family: var(--bw-font-family-sans);
background: var(--bw-color-surface);
color: var(--bw-color-fg);
-webkit-font-smoothing: antialiased;
}
@supports not (color: oklch(from red l c h)) {
body {
--nl-ink: color-mix(in oklab, var(--bw-color-accent) 35%, black);
--nl-deep: color-mix(in oklab, var(--bw-color-accent) 78%, black);
--nl-mist: var(--bw-color-accent);
--nl-glow: color-mix(in oklab, var(--bw-color-accent) 40%, white);
--nl-paper: color-mix(in oklab, var(--bw-color-accent) 4%, white);
}
}
main :where(h1, h2, h3, p, figure, blockquote, dl, dd) { margin: 0; }
/* Dark regions re-point the kit tokens so bw-btn variants read on them. */
.nl-dark {
--bw-color-accent: var(--nl-paper);
--bw-color-accent-hover: color-mix(in oklab, var(--nl-paper) 88%, var(--nl-glow));
--bw-color-fg-on-accent: var(--nl-ink);
--bw-color-fg: var(--nl-paper);
--bw-color-fg-muted: var(--nl-p72);
--bw-state-hover-overlay: var(--nl-p08);
--bw-state-active-overlay: var(--nl-p16);
color: var(--nl-paper);
}
.nl-dark .bw-btn--ghost {
color: var(--nl-paper);
border-color: color-mix(in oklab, var(--nl-paper) 36%, transparent);
}
.nl-eyebrow {
margin: 0 0 var(--bw-space-3);
font-size: var(--bw-font-size-xs);
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--bw-color-accent);
}
.nl-h2 {
font-family: var(--bw-font-family-display);
font-size: clamp(1.875rem, 3.6vw, 2.875rem);
font-weight: 650;
letter-spacing: -0.035em;
line-height: 1.06;
text-wrap: balance;
}
.nl-lede {
max-width: 40ch;
font-size: 1.0625rem;
line-height: 1.55;
color: var(--bw-color-fg-muted);
}
.nl-pill {
--pill: var(--bw-color-success);
display: inline-flex;
align-items: center;
min-height: 1.5rem;
padding: 0 0.65rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
color: color-mix(in oklab, var(--pill) 55%, var(--bw-color-fg));
background: color-mix(in oklab, var(--pill) 16%, transparent);
border: 1px solid color-mix(in oklab, var(--pill) 45%, transparent);
}
.nl-dark .nl-pill {
color: var(--nl-paper);
background: color-mix(in oklab, var(--pill) 30%, transparent);
border-color: color-mix(in oklab, var(--pill) 55%, transparent);
}
.nl-pill--late { --pill: var(--bw-color-warning); }
.nl-pill--held { --pill: var(--bw-color-danger); }
/* Header: sticky glass bar that sits over the hero. */
.nl-header {
position: sticky;
top: 0;
z-index: 30;
display: flex;
align-items: center;
gap: var(--bw-space-5);
min-height: var(--nl-header-h);
margin-bottom: calc(var(--nl-header-h) * -1);
padding: 0 var(--nl-edge);
box-sizing: border-box;
background: color-mix(in oklab, var(--nl-ink) 86%, transparent);
border-bottom: 1px solid var(--nl-p08);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
backdrop-filter: blur(16px) saturate(1.2);
}
.nl-brand {
display: inline-flex;
align-items: center;
gap: 0.65rem;
margin-right: auto;
font-family: var(--bw-font-family-display);
font-size: 1.3125rem;
font-weight: 650;
letter-spacing: -0.03em;
color: var(--nl-paper);
text-decoration: none;
}
.nl-mark {
display: grid;
place-items: center;
width: 1.75rem;
height: 1.75rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, var(--nl-glow), var(--nl-mist));
box-shadow: inset 0 1px 0 var(--nl-p16);
}
.nl-mark svg {
width: 1rem;
height: 1rem;
fill: none;
stroke: var(--nl-ink);
stroke-width: 3;
stroke-linecap: round;
stroke-linejoin: round;
}
.nl-nav { display: flex; gap: var(--bw-space-6); }
.nl-nav a {
font-size: var(--bw-font-size-sm);
font-weight: 500;
color: var(--nl-p72);
text-decoration: none;
transition: color 0.2s ease;
}
.nl-nav a:hover { color: var(--nl-paper); }
.nl-header__actions { display: flex; align-items: center; gap: var(--bw-space-2); }
/* Hero: one composition. The brand is the headline's first word. */
.nl-hero {
position: relative;
display: grid;
gap: var(--bw-space-12);
align-items: center;
min-height: min(100svh, 54rem);
box-sizing: border-box;
padding: calc(var(--nl-header-h) + clamp(2.5rem, 6vw, 4.5rem)) var(--nl-gutter) clamp(3.5rem, 8vw, 6rem) var(--nl-edge);
overflow: hidden;
background:
linear-gradient(115deg, color-mix(in oklab, var(--nl-ink) 60%, transparent) 0%, transparent 62%),
linear-gradient(165deg, var(--nl-ink) 0%, var(--nl-deep) 55%, var(--nl-mist) 130%);
}
.nl-hero::before,
.nl-hero::after {
content: "";
position: absolute;
pointer-events: none;
}
.nl-hero::before {
inset: 0;
background-image:
linear-gradient(var(--nl-p08) 1px, transparent 1px),
linear-gradient(90deg, var(--nl-p08) 1px, transparent 1px);
background-size: 4.5rem 4.5rem;
-webkit-mask-image: radial-gradient(ellipse at 72% 38%, black 0%, transparent 66%);
mask-image: radial-gradient(ellipse at 72% 38%, black 0%, transparent 66%);
opacity: 0.6;
}
.nl-hero::after {
inset: auto -10% -45% 28%;
height: 150%;
background:
radial-gradient(ellipse at 40% 40%, color-mix(in oklab, var(--nl-paper) 14%, transparent) 0%, transparent 55%),
radial-gradient(ellipse at 70% 60%, color-mix(in oklab, var(--nl-glow) 38%, transparent) 0%, transparent 50%);
}
@media (min-width: 64rem) {
.nl-hero {
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
padding-right: 0;
}
}
.nl-hero__copy,
.nl-hero__media { position: relative; z-index: 1; }
.nl-hero__heading {
margin-bottom: var(--bw-space-5);
max-width: 11.5ch;
font-family: var(--bw-font-family-display);
font-size: clamp(2.875rem, 6.2vw, 5rem);
font-weight: 650;
letter-spacing: -0.045em;
line-height: 0.98;
text-wrap: balance;
}
.nl-hero__brand { color: var(--nl-glow); }
.nl-hero__lede {
margin-bottom: var(--bw-space-8);
max-width: 34ch;
font-size: clamp(1.0625rem, 1.5vw, 1.1875rem);
line-height: 1.5;
color: var(--nl-p72);
}
.nl-hero__actions { display: flex; flex-wrap: wrap; gap: var(--bw-space-3); margin-bottom: var(--bw-space-5); }
.nl-hero__meta { font-size: var(--bw-font-size-sm); color: var(--nl-p60); }
/* The depot board: a product frame, not window chrome. */
.nl-hero__media { padding-bottom: 1.5rem; }
@media (min-width: 64rem) { .nl-hero__media { padding-bottom: 0; } }
.nl-board {
overflow: hidden;
border: 1px solid var(--nl-p16);
border-radius: 1.125rem;
background: color-mix(in oklab, var(--nl-ink) 80%, black);
box-shadow: 0 2.5rem 5rem color-mix(in oklab, black 42%, transparent), 0 0 0 1px var(--nl-p08);
}
@media (min-width: 64rem) {
.nl-board { border-right: 0; border-radius: 1.125rem 0 0 1.125rem; }
}
.nl-board__top {
display: flex;
align-items: center;
gap: var(--bw-space-4);
padding: 0.7rem 1rem;
border-bottom: 1px solid var(--nl-p08);
background: var(--nl-p08);
}
.nl-board__hub {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
font-weight: 600;
}
.nl-board__hub .nl-mark { width: 1.25rem; height: 1.25rem; border-radius: 0.35rem; }
.nl-board__hub .nl-mark svg { width: 0.7rem; height: 0.7rem; }
.nl-board__tabs {
display: flex;
gap: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
color: var(--nl-p60);
}
.nl-board__tabs span { padding: 0.3rem 0.7rem; border-radius: 999px; }
.nl-board__tabs .is-on { color: var(--nl-paper); background: var(--nl-p16); }
.nl-board__live {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin-left: auto;
font-size: 0.75rem;
font-weight: 600;
color: var(--nl-p72);
}
.nl-board__live::before {
content: "";
width: 0.5rem;
height: 0.5rem;
border-radius: 999px;
background: var(--bw-color-success);
box-shadow: 0 0 0 3px color-mix(in oklab, var(--bw-color-success) 30%, transparent);
}
.nl-board__summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
border-bottom: 1px solid var(--nl-p08);
}
.nl-board__summary > div { padding: var(--bw-space-4) var(--bw-space-5); border-right: 1px solid var(--nl-p08); }
.nl-board__summary > div:last-child { border-right: 0; }
.nl-board__figure {
font-family: var(--bw-font-family-display);
font-size: clamp(1.5rem, 2.8vw, 2.125rem);
font-weight: 650;
letter-spacing: -0.035em;
line-height: 1;
}
.nl-board__caption { margin-top: 0.4rem; font-size: 0.75rem; color: var(--nl-p60); }
.nl-board__rows {
display: grid;
gap: var(--bw-space-2);
margin: 0;
padding: var(--bw-space-3);
list-style: none;
}
.nl-row {
display: grid;
grid-template-columns: 3.25rem minmax(0, 1fr) auto;
align-items: center;
gap: var(--bw-space-4);
padding: 0.7rem var(--bw-space-4);
border: 1px solid var(--nl-p08);
border-radius: 0.75rem;
background: color-mix(in oklab, var(--nl-deep) 55%, transparent);
transition: transform 0.3s ease, background-color 0.3s ease, border-color 0.3s ease;
}
.nl-row:hover {
transform: translateX(4px);
border-color: var(--nl-p16);
background: color-mix(in oklab, var(--nl-mist) 22%, var(--nl-deep));
}
.nl-row--focus {
background: color-mix(in oklab, var(--nl-mist) 34%, var(--nl-deep));
border-color: color-mix(in oklab, var(--nl-glow) 40%, transparent);
}
.nl-row__id { font-size: 0.8125rem; font-weight: 700; letter-spacing: 0.02em; color: var(--nl-glow); }
.nl-row__route { font-size: 0.9375rem; font-weight: 600; }
.nl-row__detail { margin-top: 0.15rem; font-size: 0.75rem; color: var(--nl-p60); }
.nl-prog {
display: block;
height: 3px;
margin-top: 0.6rem;
border-radius: 2px;
background: var(--nl-p08);
overflow: hidden;
}
.nl-prog::after {
content: "";
display: block;
width: var(--p);
height: 100%;
background: var(--bar, var(--nl-glow));
}
.nl-row--late { --bar: var(--bw-color-warning); }
.nl-row--held { --bar: var(--bw-color-danger); }
.nl-board__track { display: block; width: 100%; height: auto; padding: 0 var(--bw-space-3) var(--bw-space-3); box-sizing: border-box; }
.nl-track-base { fill: none; stroke: var(--nl-p16); stroke-width: 2.5; stroke-dasharray: 2 7; stroke-linecap: round; }
.nl-track-line { fill: none; stroke: var(--nl-glow); stroke-width: 2.5; stroke-linecap: round; }
.nl-track-stop { fill: var(--nl-ink); stroke: var(--nl-glow); stroke-width: 2.5; }
.nl-track-halo { fill: color-mix(in oklab, var(--nl-glow) 30%, transparent); }
.nl-track-now { fill: var(--nl-paper); }
.nl-track-label { font-family: var(--bw-font-family-sans); font-size: 11px; font-weight: 600; fill: var(--nl-p60); }
.nl-toast {
display: flex;
gap: 0.7rem;
margin-top: var(--bw-space-3);
padding: 0.8rem 1rem;
border-radius: 0.875rem;
color: var(--nl-ink);
background: var(--nl-paper);
box-shadow: 0 1.25rem 2.5rem color-mix(in oklab, black 35%, transparent);
}
.nl-toast::before {
content: "";
flex: none;
width: 0.6rem;
height: 0.6rem;
margin-top: 0.3rem;
border-radius: 999px;
background: var(--bw-color-warning);
box-shadow: 0 0 0 4px color-mix(in oklab, var(--bw-color-warning) 28%, transparent);
}
.nl-toast p { font-size: 0.8125rem; line-height: 1.35; }
.nl-toast p:first-child { font-weight: 700; }
.nl-toast p + p { color: color-mix(in oklab, var(--nl-ink) 70%, var(--nl-paper)); }
@media (min-width: 64rem) {
.nl-toast { position: absolute; left: -2.25rem; bottom: -2.25rem; margin: 0; }
}
/* Proof strip: quiet wordmarks on a light band. */
.nl-proof {
padding: clamp(2.5rem, 5vw, 3.5rem) var(--nl-edge);
border-bottom: 1px solid var(--bw-color-border);
}
.nl-proof__label {
display: flex;
align-items: center;
gap: var(--bw-space-5);
margin: 0 0 var(--bw-space-6);
font-size: var(--bw-font-size-xs);
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--bw-color-fg-muted);
}
.nl-proof__label::before,
.nl-proof__label::after { content: ""; flex: 1; height: 1px; background: var(--bw-color-border); }
.nl-proof__list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--bw-space-6) var(--bw-space-4);
margin: 0;
padding: 0;
list-style: none;
}
@media (min-width: 48rem) { .nl-proof__list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .nl-proof__list { grid-template-columns: repeat(6, minmax(0, 1fr)); } }
.nl-wordmark {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 1.0625rem;
font-weight: 700;
letter-spacing: -0.02em;
white-space: nowrap;
color: color-mix(in oklab, var(--bw-color-fg) 66%, var(--bw-color-surface));
transition: color 0.25s ease;
}
.nl-wordmark:hover { color: var(--bw-color-accent); }
.nl-wordmark svg {
width: 1.375rem;
height: 1.375rem;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* Product: editorial header plus three working miniatures. */
.nl-product { padding: var(--nl-y) var(--nl-edge); }
.nl-product__head {
display: grid;
gap: var(--bw-space-5);
align-items: end;
margin-bottom: clamp(2.5rem, 5vw, 4rem);
}
@media (min-width: 64rem) {
.nl-product__head { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); column-gap: clamp(3rem, 7vw, 6rem); }
.nl-product__head .nl-lede { justify-self: end; }
}
.nl-product__head .nl-h2 { max-width: 15ch; }
.nl-feats { display: grid; gap: var(--bw-space-6); }
@media (min-width: 64rem) { .nl-feats { grid-template-columns: 1.2fr 1fr 1fr; } }
.nl-feat { display: flex; flex-direction: column; gap: var(--bw-space-3); transition: transform 0.35s ease; }
.nl-feat:hover { transform: translateY(-5px); }
.nl-mini {
display: grid;
align-content: center;
gap: var(--bw-space-3);
min-height: 12.5rem;
margin-bottom: var(--bw-space-2);
padding: var(--bw-space-5);
border: 1px solid var(--bw-color-border);
border-radius: 1.125rem;
background: radial-gradient(ellipse at 100% 0%, var(--nl-tint-2) 0%, transparent 60%), var(--nl-tint);
transition: border-color 0.35s ease, box-shadow 0.35s ease;
}
.nl-feat:hover .nl-mini {
border-color: color-mix(in oklab, var(--bw-color-accent) 35%, var(--bw-color-border));
box-shadow: 0 1.25rem 2.5rem color-mix(in oklab, var(--bw-color-accent) 14%, transparent);
}
.nl-feat h3,
.nl-surface h3 {
font-family: var(--bw-font-family-display);
font-size: 1.375rem;
font-weight: 650;
letter-spacing: -0.025em;
}
.nl-feat p,
.nl-surface figcaption p { max-width: 36ch; line-height: 1.55; color: var(--bw-color-fg-muted); }
.nl-m-route {
display: grid;
grid-template-columns: 2.75rem minmax(0, 1fr) auto;
align-items: center;
gap: var(--bw-space-3);
padding: 0.55rem 0.75rem;
border: 1px solid var(--bw-color-border);
border-radius: 0.625rem;
background: var(--bw-color-surface);
font-size: 0.75rem;
}
.nl-m-route b { color: var(--bw-color-accent); }
.nl-m-track { position: relative; display: block; height: 4px; border-radius: 2px; background: var(--bw-color-border); }
.nl-m-track::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: var(--p);
border-radius: 2px;
background: var(--bw-color-accent);
}
.nl-m-track::after {
content: "";
position: absolute;
top: 50%;
left: var(--p);
width: 0.6rem;
height: 0.6rem;
border-radius: 999px;
background: var(--bw-color-surface);
border: 2px solid var(--bw-color-accent);
transform: translate(-50%, -50%);
}
.nl-m-hand { display: flex; align-items: center; justify-content: center; gap: var(--bw-space-3); }
.nl-av {
display: grid;
place-items: center;
flex: none;
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
font-size: 0.8125rem;
font-weight: 700;
color: var(--bw-color-fg-on-accent);
background: var(--bw-color-accent);
}
.nl-av--b {
color: var(--bw-color-accent);
background: var(--bw-color-surface);
border: 1px solid color-mix(in oklab, var(--bw-color-accent) 35%, var(--bw-color-border));
}
.nl-m-arrow {
position: relative;
flex: 0 1 5rem;
height: 2px;
background: repeating-linear-gradient(90deg, var(--bw-color-accent) 0 6px, transparent 6px 11px);
}
.nl-m-arrow::after {
content: "";
position: absolute;
right: -1px;
top: 50%;
width: 0.5rem;
height: 0.5rem;
border: solid var(--bw-color-accent);
border-width: 2px 2px 0 0;
transform: translateY(-50%) rotate(45deg);
}
.nl-m-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--bw-space-2); }
.nl-m-chips span {
padding: 0.3rem 0.7rem;
border: 1px solid var(--bw-color-border);
border-radius: 999px;
background: var(--bw-color-surface);
font-size: 0.75rem;
font-weight: 600;
}
.nl-bays { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--bw-space-2); }
.nl-bay {
--state: var(--bw-color-success);
display: grid;
place-items: center;
aspect-ratio: 1.7;
border: 1px solid color-mix(in oklab, var(--state) 40%, var(--bw-color-border));
border-radius: 0.625rem;
background: color-mix(in oklab, var(--state) 12%, var(--bw-color-surface));
font-size: 0.8125rem;
font-weight: 700;
}
.nl-bay--warn { --state: var(--bw-color-warning); }
.nl-bay--held { --state: var(--bw-color-danger); }
.nl-bays__note { font-size: 0.75rem; font-weight: 600; text-align: center; color: var(--bw-color-fg-muted); }
/* Depth band: one status, three surfaces. */
.nl-depth {
padding: var(--nl-y) var(--nl-edge);
border-block: 1px solid var(--bw-color-border);
background: radial-gradient(ellipse at 50% 0%, var(--nl-tint-2) 0%, transparent 60%), var(--nl-tint);
}
.nl-depth__head { max-width: 40rem; margin: 0 auto clamp(2.5rem, 5vw, 4rem); text-align: center; }
.nl-depth__head .nl-lede { margin: var(--bw-space-4) auto 0; }
.nl-triptych { display: grid; gap: var(--bw-space-8); }
@media (min-width: 48rem) { .nl-triptych { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.nl-surface { display: flex; flex-direction: column; gap: var(--bw-space-4); }
.nl-surface h3 { font-size: 1.0625rem; letter-spacing: -0.01em; font-family: inherit; }
.nl-surface figcaption p { margin-top: 0.25rem; font-size: var(--bw-font-size-sm); line-height: 1.5; }
.nl-mock {
flex: 1;
display: grid;
align-content: center;
gap: var(--bw-space-3);
min-height: 13rem;
padding: var(--bw-space-5);
border: 1px solid var(--bw-color-border);
border-radius: 1.125rem;
background: var(--bw-color-surface);
box-shadow: 0 1.25rem 3rem color-mix(in oklab, var(--nl-ink) 10%, transparent);
transition: transform 0.35s ease, box-shadow 0.35s ease;
}
.nl-surface:hover .nl-mock {
transform: translateY(-4px);
box-shadow: 0 1.75rem 3.5rem color-mix(in oklab, var(--nl-ink) 18%, transparent);
}
.nl-mock__label {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--bw-color-fg-muted);
}
.nl-mock__row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--bw-space-3);
padding: 0.7rem 0.85rem;
border: 1px solid color-mix(in oklab, var(--bw-color-accent) 30%, var(--bw-color-border));
border-radius: 0.75rem;
background: var(--nl-tint);
}
.nl-mock__row--quiet { border-color: var(--bw-color-border); background: var(--bw-color-surface); }
.nl-mock__row b { font-size: 0.8125rem; color: var(--bw-color-accent); }
.nl-mock__row strong { display: block; font-size: 0.875rem; }
.nl-mock small { font-size: 0.75rem; line-height: 1.4; color: var(--bw-color-fg-muted); }
.nl-phone {
display: grid;
gap: 0.5rem;
width: min(100%, 11rem);
margin-inline: auto;
padding: 0.9rem 0.8rem 0.8rem;
border: 5px solid var(--nl-ink);
border-radius: 1.75rem;
background: var(--bw-color-surface);
}
.nl-phone p { font-size: 0.75rem; }
.nl-phone__line { padding: 0.4rem 0.55rem; border-radius: 0.5rem; background: var(--nl-tint); color: var(--bw-color-fg-muted); }
.nl-phone__cta {
padding: 0.5rem;
border-radius: 0.5rem;
font-weight: 700;
text-align: center;
color: var(--bw-color-fg-on-accent);
background: var(--bw-color-accent);
}
.nl-eta {
font-family: var(--bw-font-family-display);
font-size: 2.75rem;
font-weight: 650;
letter-spacing: -0.045em;
line-height: 1;
color: var(--bw-color-accent);
}
/* Voice: one quote, three numbers. */
.nl-voice { display: grid; gap: var(--bw-space-12); padding: var(--nl-y) var(--nl-edge); }
@media (min-width: 64rem) {
.nl-voice { grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr); column-gap: clamp(3rem, 8vw, 7rem); align-items: center; }
}
.nl-quote::before {
content: "\201C";
content: "\201C" / "";
display: block;
height: 3.5rem;
font-family: var(--bw-font-family-display);
font-size: 7rem;
line-height: 1;
color: var(--bw-color-accent);
opacity: 0.35;
}
.nl-quote blockquote {
margin-bottom: var(--bw-space-6);
max-width: 26ch;
font-family: var(--bw-font-family-display);
font-size: clamp(1.75rem, 3.4vw, 2.75rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 1.15;
text-wrap: balance;
}
.nl-quote figcaption {
display: flex;
align-items: center;
gap: var(--bw-space-4);
font-size: var(--bw-font-size-sm);
color: var(--bw-color-fg-muted);
}
.nl-quote strong { display: block; font-size: 1rem; color: var(--bw-color-fg); }
.nl-stat {
display: flex;
flex-direction: column;
padding: var(--bw-space-5) 0;
border-top: 1px solid var(--bw-color-border);
transition: border-color 0.3s ease;
}
.nl-stat:last-child { border-bottom: 1px solid var(--bw-color-border); }
.nl-stat:hover { border-top-color: var(--bw-color-accent); }
.nl-stat dd {
order: 1;
font-family: var(--bw-font-family-display);
font-size: clamp(2.5rem, 4.4vw, 3.5rem);
font-weight: 650;
letter-spacing: -0.045em;
line-height: 1;
color: var(--bw-color-accent);
}
.nl-stat dt { order: 2; margin-top: var(--bw-space-2); color: var(--bw-color-fg-muted); }
/* Close and footer. */
.nl-close {
position: relative;
padding: var(--nl-y) var(--nl-edge);
overflow: hidden;
background: radial-gradient(ellipse at 85% 0%, color-mix(in oklab, var(--nl-glow) 22%, transparent) 0%, transparent 55%), var(--nl-ink);
}
.nl-close__ghost {
position: absolute;
inset: auto var(--nl-gutter) -0.2em auto;
font-family: var(--bw-font-family-display);
font-size: clamp(5rem, 17vw, 15rem);
font-weight: 700;
letter-spacing: -0.06em;
line-height: 1;
color: transparent;
-webkit-text-stroke: 1px var(--nl-p08);
user-select: none;
pointer-events: none;
}
.nl-close__inner {
position: relative;
display: flex;
flex-wrap: wrap;
gap: var(--bw-space-8);
align-items: end;
justify-content: space-between;
}
.nl-close h2 {
margin-bottom: var(--bw-space-4);
max-width: 17ch;
font-family: var(--bw-font-family-display);
font-size: clamp(2.25rem, 4.6vw, 3.75rem);
font-weight: 650;
letter-spacing: -0.04em;
line-height: 1.02;
text-wrap: balance;
}
.nl-close p { max-width: 38ch; line-height: 1.5; color: var(--nl-p72); }
.nl-close__actions { display: flex; flex-wrap: wrap; gap: var(--bw-space-3); }
.nl-footer {
display: grid;
gap: var(--bw-space-8);
padding: clamp(2.5rem, 5vw, 3.5rem) var(--nl-edge) var(--bw-space-6);
border-top: 1px solid var(--nl-p08);
background: var(--nl-ink);
font-size: var(--bw-font-size-sm);
color: var(--nl-p60);
}
@media (min-width: 48rem) {
.nl-footer { grid-template-columns: 2fr 1fr 1fr; }
.nl-footer__base { grid-column: 1 / -1; }
}
.nl-footer p { max-width: 30ch; margin: 0.75rem 0 0; line-height: 1.5; }
.nl-footer h2 {
margin: 0 0 var(--bw-space-3);
font-size: var(--bw-font-size-xs);
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--nl-paper);
}
.nl-footer ul { display: grid; gap: var(--bw-space-2); margin: 0; padding: 0; list-style: none; }
.nl-footer a:not(.nl-brand) { color: inherit; text-decoration: none; transition: color 0.2s ease; }
.nl-footer a:not(.nl-brand):hover { color: var(--nl-paper); }
.nl-footer__base { padding-top: var(--bw-space-5); border-top: 1px solid var(--nl-p08); }
.nl-footer .nl-footer__base p { max-width: none; margin: 0; }
/* Motion: entrance, route draw, scroll reveal. All opt-out. */
@media (prefers-reduced-motion: no-preference) {
@keyframes nl-rise {
from { opacity: 0; transform: translateY(1.5rem); }
to { opacity: 1; transform: none; }
}
@keyframes nl-draw {
from { stroke-dashoffset: 1; }
to { stroke-dashoffset: 0; }
}
.nl-hero__copy > * { animation: nl-rise 0.8s cubic-bezier(0.2, 0, 0, 1) both; }
.nl-hero__copy > :nth-child(2) { animation-delay: 0.1s; }
.nl-hero__copy > :nth-child(3) { animation-delay: 0.2s; }
.nl-hero__copy > :nth-child(4) { animation-delay: 0.3s; }
.nl-hero__media { animation: nl-rise 1s 0.25s cubic-bezier(0.2, 0, 0, 1) both; }
.nl-track-line {
stroke-dasharray: 1;
animation: nl-draw 1.8s 0.9s ease-out both;
}
@supports (animation-timeline: view()) {
.nl-reveal {
animation: nl-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 38%;
}
}
}
@media (max-width: 48rem) {
.nl-nav,
.nl-board__tabs,
.nl-row__id,
.nl-header__actions .bw-btn--ghost { display: none; }
.nl-hero { min-height: 0; }
.nl-board__summary > div { padding: var(--bw-space-3); }
.nl-row { grid-template-columns: minmax(0, 1fr) auto; }
.nl-close__inner { align-items: start; }
}
</style>
</head>
<body data-theme="light" data-bw-brand="northline">
<a class="bw-skip-link" href="#main">Skip to content</a>
<svg width="0" height="0" style="position: absolute" aria-hidden="true" focusable="false">
<symbol id="nl-glyph" viewBox="0 0 24 24"><path d="M5 19V5l14 14V5" /></symbol>
</svg>
<header class="nl-header nl-dark">
<a class="nl-brand" href="/templates/homepage/full/">
<span class="nl-mark" aria-hidden="true"><svg><use href="#nl-glyph" /></svg></span>
Northline
</a>
<nav class="nl-nav" aria-label="Primary">
<a href="#product">Product</a>
<a href="#customers">Customers</a>
<a href="/templates/pricing/full/">Pricing</a>
</nav>
<div class="nl-header__actions">
<a class="bw-btn bw-btn--ghost" href="/templates/auth-signin/full/">Sign in</a>
<a class="bw-btn bw-btn--primary" href="/templates/contact/full/">Book a walkthrough</a>
</div>
</header>
<main id="main">
<section class="nl-hero nl-dark" aria-labelledby="nl-hero-heading">
<div class="nl-hero__copy">
<h1 id="nl-hero-heading" class="nl-hero__heading">
<span class="nl-hero__brand">Northline</span> keeps every route on time.
</h1>
<p class="nl-hero__lede">
One live board for every route, driver and bay, so late loads are handled before a customer has to ring.
</p>
<div class="nl-hero__actions">
<a class="bw-btn bw-btn--primary bw-btn--lg" href="/templates/contact/full/">Start free trial</a>
<a class="bw-btn bw-btn--ghost bw-btn--lg" href="#product">See the product</a>
</div>
<p class="nl-hero__meta">Fourteen days, no card. Your first depot live by Friday.</p>
</div>
<div class="nl-hero__media">
<div class="nl-board" role="img" aria-label="Northline depot board for the Mersey hub: forty-two routes today, ninety-four per cent on time, one route running twelve minutes late and one held at a bay">
<div class="nl-board__top" aria-hidden="true">
<span class="nl-board__hub">
<span class="nl-mark"><svg><use href="#nl-glyph" /></svg></span>
Mersey hub
</span>
<span class="nl-board__tabs"><span class="is-on">Routes</span><span>Handoffs</span><span>Bays</span></span>
<span class="nl-board__live">Live</span>
</div>
<div class="nl-board__summary" aria-hidden="true">
<div><p class="nl-board__figure">42</p><p class="nl-board__caption">Routes today</p></div>
<div><p class="nl-board__figure">94%</p><p class="nl-board__caption">On time</p></div>
<div><p class="nl-board__figure">2</p><p class="nl-board__caption">Need action</p></div>
</div>
<ul class="nl-board__rows" aria-hidden="true">
<li class="nl-row">
<span class="nl-row__id">R-218</span>
<div>
<p class="nl-row__route">Liverpool to Preston</p>
<p class="nl-row__detail">A. Okafor · Next stop 11:40</p>
<span class="nl-prog" style="--p: 72%"></span>
</div>
<span class="nl-pill">On time</span>
</li>
<li class="nl-row nl-row--focus nl-row--late">
<span class="nl-row__id">R-224</span>
<div>
<p class="nl-row__route">Warrington to Leeds</p>
<p class="nl-row__detail">Handoff to evening shift at 14:10</p>
<span class="nl-prog" style="--p: 58%"></span>
</div>
<span class="nl-pill nl-pill--late">12 min late</span>
</li>
<li class="nl-row nl-row--held">
<span class="nl-row__id">R-231</span>
<div>
<p class="nl-row__route">Chester to Manchester</p>
<p class="nl-row__detail">Waiting for bay 4 to clear</p>
<span class="nl-prog" style="--p: 31%"></span>
</div>
<span class="nl-pill nl-pill--held">Held at bay</span>
</li>
</ul>
<svg class="nl-board__track" viewBox="0 0 520 70" aria-hidden="true" focusable="false">
<path class="nl-track-base" d="M20 44 C 110 44, 130 20, 220 24 S 360 52, 500 30" />
<path class="nl-track-line" pathLength="1" d="M20 44 C 110 44, 130 20, 220 24 S 300 40, 330 40" />
<circle class="nl-track-stop" cx="20" cy="44" r="5" />
<circle class="nl-track-stop" cx="220" cy="24" r="5" />
<circle class="nl-track-halo" cx="330" cy="40" r="13" />
<circle class="nl-track-now" cx="330" cy="40" r="5.5" />
<circle class="nl-track-stop" cx="500" cy="30" r="5" />
<text class="nl-track-label" x="8" y="64">Depot</text>
<text class="nl-track-label" x="200" y="12">Handoff</text>
<text class="nl-track-label" x="300" y="66">Van 224</text>
<text class="nl-track-label" x="458" y="54">Customer</text>
</svg>
</div>
<aside class="nl-toast" aria-hidden="true">
<div>
<p>R-224 arrival moved to 14:22</p>
<p>Customer told automatically · 12:48</p>
</div>
</aside>
</div>
</section>
<section id="customers" class="nl-proof" aria-labelledby="nl-proof-label">
<p id="nl-proof-label" class="nl-proof__label">Running depots across the north</p>
<ul class="nl-proof__list">
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 14c3-5 5 5 8 0s5 5 8 0 3 2 4 0M12 3v6" /></svg>Harbour Freight</li>
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 20h14M7 15h10M10 10h4" /></svg>Cairn Logistics</li>
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="9" cy="12" r="6" /><circle cx="15" cy="12" r="6" /></svg>Fenwick & Ward</li>
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 18 12 5l8 13z" /></svg>Tidewater Haulage</li>
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v18M4 7.5l16 9M4 16.5l16-9" /></svg>Calder Cold Chain</li>
<li class="nl-wordmark"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 8 12 4l8 4v9l-8 4-8-4zM4 8l8 4 8-4M12 12v9" /></svg>Marlow Parcels</li>
</ul>
</section>
<section id="product" class="nl-product" aria-labelledby="nl-product-heading">
<div class="nl-product__head">
<div>
<p class="nl-eyebrow">The product</p>
<h2 id="nl-product-heading" class="nl-h2">Everything a depot office does all day, done once.</h2>
</div>
<p class="nl-lede">
Northline replaces the whiteboard, the group chat and the status phone call with one board every shift can trust.
</p>
</div>
<div class="nl-feats">
<article class="nl-feat nl-reveal">
<div class="nl-mini" aria-hidden="true">
<div class="nl-m-route"><b>R-218</b><span class="nl-m-track" style="--p: 72%"></span><span class="nl-pill">On time</span></div>
<div class="nl-m-route"><b>R-224</b><span class="nl-m-track" style="--p: 58%"></span><span class="nl-pill nl-pill--late">12 min late</span></div>
<div class="nl-m-route"><b>R-231</b><span class="nl-m-track" style="--p: 31%"></span><span class="nl-pill nl-pill--held">Held</span></div>
</div>
<h3>Live route boards</h3>
<p>Every route, ETA and bay on one screen. Late loads rise to the top while there is still time to act.</p>
</article>
<article class="nl-feat nl-reveal">
<div class="nl-mini" aria-hidden="true">
<div class="nl-m-hand"><span class="nl-av">AO</span><span class="nl-m-arrow"></span><span class="nl-av nl-av--b">MH</span></div>
<div class="nl-m-chips"><span>3 notes</span><span>2 photos</span><span>1 signature</span></div>
</div>
<h3>Driver handoffs</h3>
<p>Notes, photos and signatures travel with the job, so the evening shift starts where the morning left off.</p>
</article>
<article class="nl-feat nl-reveal">
<div class="nl-mini" aria-hidden="true">
<div class="nl-bays">
<span class="nl-bay">1</span><span class="nl-bay">2</span><span class="nl-bay">3</span>
<span class="nl-bay nl-bay--held">4</span><span class="nl-bay nl-bay--warn">5</span><span class="nl-bay">6</span>
</div>
<p class="nl-bays__note">Bay 4 clears at 13:20</p>
</div>
<h3>Depot alerts</h3>
<p>Bay delays and capacity risks are flagged ahead of the afternoon rush, with the next free slot suggested.</p>
</article>
</div>
</section>
<section class="nl-depth" aria-labelledby="nl-depth-heading">
<div class="nl-depth__head">
<p class="nl-eyebrow">One shared status</p>
<h2 id="nl-depth-heading" class="nl-h2">Depot, driver and customer read the same story.</h2>
<p class="nl-lede">
A change lands on the board once. The depot office, the driver app and the customer link all read it from there.
</p>
</div>
<div class="nl-triptych">
<figure class="nl-surface nl-reveal">
<div class="nl-mock" aria-hidden="true">
<p class="nl-mock__label">Depot board</p>
<div class="nl-mock__row">
<b>R-224</b>
<div><strong>Warrington to Leeds</strong><small>Arrival 14:22</small></div>
<span class="nl-pill nl-pill--late">12 min late</span>
</div>
<div class="nl-mock__row nl-mock__row--quiet">
<b>R-218</b>
<div><strong>Liverpool to Preston</strong><small>Next stop 11:40</small></div>
<span class="nl-pill">On time</span>
</div>
</div>
<figcaption>
<h3>Depot office</h3>
<p>The board row updates the moment a driver flags a delay.</p>
</figcaption>
</figure>
<figure class="nl-surface nl-reveal">
<div class="nl-mock" aria-hidden="true">
<div class="nl-phone">
<p><b>R-224</b></p>
<p class="nl-phone__line">Handoff at 14:10</p>
<p class="nl-phone__line">3 notes · 2 photos</p>
<p class="nl-phone__cta">Hand over</p>
</div>
</div>
<figcaption>
<h3>Driver app</h3>
<p>The handoff, notes and photos stay attached to the route.</p>
</figcaption>
</figure>
<figure class="nl-surface nl-reveal">
<div class="nl-mock" aria-hidden="true">
<p class="nl-mock__label">Your delivery</p>
<p class="nl-eta">14:22</p>
<small>Twelve minutes later than planned. We will update you if that changes.</small>
<span class="nl-m-track" style="--p: 58%"></span>
</div>
<figcaption>
<h3>Customer link</h3>
<p>The customer is told once, with a new arrival time.</p>
</figcaption>
</figure>
</div>
</section>
<section class="nl-voice" aria-labelledby="nl-voice-heading">
<h2 id="nl-voice-heading" class="bw-visually-hidden">What customers say</h2>
<figure class="nl-quote nl-reveal">
<blockquote>We stopped chasing status calls. Dispatch, drivers and customers all read the same board.</blockquote>
<figcaption>
<span class="nl-av" aria-hidden="true">PR</span>
<span><strong>Priya Raman</strong>Head of Operations, Harbour Freight</span>
</figcaption>
</figure>
<dl class="nl-reveal">
<div class="nl-stat"><dt>Fewer late arrivals</dt><dd>18%</dd></div>
<div class="nl-stat"><dt>Average handoff time</dt><dd>6 min</dd></div>
<div class="nl-stat"><dt>Shared by every shift</dt><dd>1 board</dd></div>
</dl>
</section>
<section class="nl-close nl-dark" aria-labelledby="nl-close-heading">
<span class="nl-close__ghost" aria-hidden="true">Northline</span>
<div class="nl-close__inner">
<div>
<h2 id="nl-close-heading">Put your next depot on the board.</h2>
<p>Start the trial today and invite the whole ops team. Your own routes can be live within a day.</p>
</div>
<div class="nl-close__actions">
<a class="bw-btn bw-btn--primary bw-btn--lg" href="/templates/contact/full/">Start free trial</a>
<a class="bw-btn bw-btn--ghost bw-btn--lg" href="/templates/pricing/full/">See pricing</a>
</div>
</div>
</section>
</main>
<footer class="nl-footer nl-dark">
<div>
<a class="nl-brand" href="/templates/homepage/full/">
<span class="nl-mark" aria-hidden="true"><svg><use href="#nl-glyph" /></svg></span>
Northline
</a>
<p>Depot and fleet operations software for ops teams and drivers.</p>
</div>
<nav aria-label="Product">
<h2>Product</h2>
<ul>
<li><a href="/templates/landing/full/">Overview</a></li>
<li><a href="/templates/pricing/full/">Pricing</a></li>
<li><a href="/templates/auth-signin/full/">Sign in</a></li>
</ul>
</nav>
<nav aria-label="Company">
<h2>Company</h2>
<ul>
<li><a href="/templates/about/full/">About</a></li>
<li><a href="/templates/contact/full/">Contact</a></li>
<li><a href="/templates/legal/full/">Legal</a></li>
</ul>
</nav>
<div class="nl-footer__base"><p>© Northline. Built for the people who keep depots moving.</p></div>
</footer>
</body>
</html>
Named export from @bwui/react. Do not invent other page components.
import "@bwui/css";
import { Homepage } from "@bwui/react";
export default function Page() {
return (
<Homepage
brand="Your brand"
title="Beautiful interfaces, out of the box"
description="Install Brickwork, copy this page, recolour it, own it."
/>
);
}