The 5 files this component needs and nothing else, each fenced with the path it belongs at, followed by what the library's own verification covered and what it did not. No instructions attached — for placing it yourself, or handing it to something that has its own.
Read it first
Install kit: "Dashboard glass hero"
Library page: https://astro.baysixmedia.com/components/dashboard-glass-hero/
Files: 5 required, 1 supporting
Write each block below to the path named in its header, relative to the project root. A block marked (required) is one the component does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.
===== FILE (required): src/components/DashboardGlassHero.astro =====
---
/**
* DashboardGlassHero
*
* An 860px full-bleed photo hero under a dark glass wash. Left: a pill
* eyebrow, a four-line display headline whose alternate lines are lime, a
* paragraph, two pill buttons, a row of tracked labels, a "network signal"
* pill and a trust card with three round avatars. Right: a translucent glass
* operations dashboard — status title, four stat cells, a four-up live
* network row and an expandable forecast row.
*
* Layout: side by side from 1200px, dashboard stacked under the copy from
* 810px, dashboard hidden below 810px (as the reference does on a phone).
* Leaves 88px at the top for an overlay header.
*
* No script. The load-in is a CSS animation that ends at rest (so no-JS
* shows everything) and is skipped under prefers-reduced-motion. The
* forecast row is a native <details> disclosure.
*
* Reads --nv-* design tokens from any ancestor, with fallbacks.
*/
type IconName =
| "shield"
| "clock"
| "leaf"
| "box"
| "road"
| "ship"
| "plane"
| "warehouse";
export interface DashboardGlassHeroStat {
label: string;
value: string;
note?: string;
icon?: IconName;
/** Any CSS colour. Defaults cycle lime, aqua, green, purple. */
color?: string;
}
export interface DashboardGlassHeroNetworkItem {
label: string;
value: string;
icon?: IconName;
color?: string;
}
export interface DashboardGlassHeroProps {
eyebrow?: string;
/**
* One entry per line. Lines alternate white / lime, starting white.
* Trailing punctuation on a line is drawn white, as measured.
*/
headline?: string[];
body?: string;
primaryCta?: { label: string; href: string } | null;
secondaryCta?: { label: string; href: string } | null;
/** The tracked labels under the buttons, separated by lime dots. */
labels?: string[];
/** The "network signal" pill. "" drops it. */
signal?: string;
/**
* The trust card: real social proof only. Omit it (or pass `null`) and
* there is no card. Nothing in it has a default — no title, no line, no
* stars, no faces — because a claim nobody made is not a placeholder.
*/
trust?: {
title?: string;
text?: string;
/** Five stars after the title. Only when `true`: a rating is never assumed. */
stars?: boolean;
avatars?: { src?: string; alt?: string }[];
} | null;
/** The photo behind everything. Omitted, or `src: ""`, renders a labelled placeholder. */
background?: { src?: string; alt?: string };
/**
* `null` drops the dashboard; the copy stays left-aligned. So does a
* dashboard with no eyebrow, title, stats, network or insight: nothing in
* it is filled in for you.
*/
dashboard?: {
eyebrow?: string;
title?: string;
stats?: DashboardGlassHeroStat[];
networkTitle?: string;
network?: DashboardGlassHeroNetworkItem[];
/**
* The forecast row. With `detail` it is a disclosure that reveals the
* detail; with `href` it is a link; with neither it is static text.
*/
insight?: { label: string; text: string; detail?: string; href?: string } | null;
} | null;
/** Plays the load-in rise. Skipped under reduced motion regardless. */
animate?: boolean;
fontStylesheet?: string;
id?: string;
}
interface Props extends DashboardGlassHeroProps {}
/* No copy, figures, photo or links of its own: the catalog demo passes them
through the registry's props, so a copied hero never ships an invented
business or an invented number. */
const {
eyebrow = "",
headline = [],
body = "",
primaryCta = null,
secondaryCta = null,
labels = [],
signal = "",
trust = null,
background = {},
dashboard = {},
animate = true,
fontStylesheet = "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap",
id,
} = Astro.props;
/* Only the photo passed. */
const bg = { src: background?.src ?? "", alt: background?.alt ?? "" };
/* A button needs both halves: a label with no destination, or the reverse, is not drawn. */
const link = (l: { label: string; href: string } | null | undefined) => (l && l.label && l.href ? l : null);
const primary = link(primaryCta);
const secondary = link(secondaryCta);
/*
* Only what the caller supplied. A card with nothing in it is not drawn, and
* stars appear only on an explicit `stars: true` - the component never
* manufactures a rating, a customer count or a face.
*/
const trustData =
trust && (trust.title || trust.text || (trust.avatars && trust.avatars.length > 0))
? { title: trust.title, text: trust.text, stars: trust.stars === true, avatars: trust.avatars ?? [] }
: null;
const TONES = ["#c8ff38", "#73f5df", "#73ef80", "#a88cff"];
/*
* The dashboard shows only what the caller passed: no status line, figure,
* network count or forecast is ever filled in, so an invented 98.6% never
* sits beside real numbers. The network heading is generic chrome and is
* the one thing that still merges. A dashboard with nothing in it is not
* drawn.
*/
const dash = dashboard
? {
networkTitle: "Live network",
...dashboard,
}
: null;
const showDash =
!!dash && !!(dash.eyebrow || dash.title || (dash.stats && dash.stats.length > 0) || (dash.network && dash.network.length > 0) || dash.insight);
const uid = id ?? `dgh-${Math.random().toString(36).slice(2, 8)}`;
const headingId = `${uid}-title`;
const insightId = `${uid}-insight`;
/* Split trailing punctuation off a headline line so it can be drawn white. */
const lines = headline.map((line, i) => {
const m = line.match(/^(.*?)([.!?,;:…]+)$/);
return { word: m ? m[1] : line, punct: m ? m[2] : "", accent: i % 2 === 1 };
});
const ICONS: Record<IconName, string> = {
shield: '<path d="M12 3l7 3v5.2c0 4.3-2.9 7.8-7 9.8-4.1-2-7-5.5-7-9.8V6l7-3z"/><path d="M9 12l2 2 4-4"/>',
clock: '<circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.8 1.8"/>',
leaf: '<path d="M5 19c0-8 6-14 14-14 0 8-6 14-14 14z"/><path d="M5 19l7.5-7.5"/>',
box: '<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9L12 3z"/><path d="M4 7.5l8 4.5 8-4.5M12 12v9"/>',
road: '<path d="M9 3L5 21M15 3l4 18M12 4v2.5M12 10.5v3M12 17.5v3"/>',
ship: '<path d="M3 15.5l2.2 4.5h13.6l2.2-4.5H3z"/><path d="M6 15.5V10h12v5.5M9.5 10V6.5h5V10M12 3.5v3"/>',
plane: '<path d="M10.5 20l1.5-.8 1.5.8M12 3.5c.8 0 1.3.7 1.3 1.6V10l7.2 4v1.8l-7.2-2.1v4.3l2 1.5V20L12 19.2 9.2 20v-.5l2-1.5v-4.3L4 15.8V14l7.2-4V5.1c0-.9.5-1.6.8-1.6z"/>',
warehouse: '<path d="M3 21V9l9-5 9 5v12"/><path d="M7 21v-8h10v8M7 17h10"/>',
};
const iconFor = (name: IconName | undefined, fallback: IconName) => ICONS[name ?? fallback] ?? ICONS[fallback];
const STAT_FALLBACK: IconName[] = ["shield", "clock", "leaf", "box"];
const NET_FALLBACK: IconName[] = ["road", "ship", "plane", "warehouse"];
const arrow = '<path d="M7 17L17 7M8.5 7H17v8.5"/>';
/* A photo alone is not a hero: with no copy, link, proof or dashboard, render nothing. */
const hasUtility = labels.length > 0 || !!signal || !!trustData;
const hasContent = !!eyebrow || lines.length > 0 || !!body || !!primary || !!secondary || hasUtility || showDash;
---
{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<section
class:list={["dgh", { "dgh--animate": animate }]}
id={id}
aria-labelledby={lines.length > 0 ? headingId : undefined}
>
<div class="dgh-bg">
{
bg.src ? (
<img
src={bg.src}
alt={bg.alt ?? ""}
width="2000"
height="1200"
loading="eager"
fetchpriority="high"
decoding="async"
/>
) : (
<div class="dgh-ph" role="img" aria-label="Image placeholder: wide landscape photo">
<div class="dgh-ph-text" aria-hidden="true">
<span class="dgh-ph-label">Background photo</span>
<span class="dgh-ph-hint">Wide landscape photo</span>
<span class="dgh-ph-spec">5:3 · 2000 × 1200 px</span>
</div>
</div>
)
}
</div>
<div class="dgh-wash" aria-hidden="true"></div>
<div class="dgh-inner">
<div class="dgh-copy">
{
eyebrow && (
<p class="dgh-eyebrow">
<span class="dgh-dot" aria-hidden="true" />
{eyebrow}
</p>
)
}
{lines.length > 0 && (
<h1 class="dgh-title" id={headingId}>
{
lines.map((l) => (
<span class:list={["dgh-line", { "dgh-line--accent": l.accent }]}>
{l.word}{l.punct && <span class="dgh-punct">{l.punct}</span>}
</span>
))
}
</h1>
)}
{body && <p class="dgh-body">{body}</p>}
{
(primary || secondary) && (
<div class="dgh-ctas">
{primary && (
<a class="dgh-btn dgh-btn--primary" href={primary.href}>
<span>{primary.label}</span>
<svg class="dgh-btn-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={arrow} />
</a>
)}
{secondary && (
<a class="dgh-btn dgh-btn--secondary" href={secondary.href}>
<span>{secondary.label}</span>
<svg class="dgh-btn-icon dgh-btn-icon--sm" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={arrow} />
</a>
)}
</div>
)
}
{hasUtility && (
<div class="dgh-utility">
{
labels.length > 0 && (
<ul class="dgh-labels">
{labels.map((label) => (
<li>{label}</li>
))}
</ul>
)
}
{
signal && (
<p class="dgh-signal">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="1.8" fill="currentColor" stroke="none" />
<path class="dgh-wave dgh-wave--1" d="M8.6 8.6a4.8 4.8 0 0 0 0 6.8M15.4 8.6a4.8 4.8 0 0 1 0 6.8" />
<path class="dgh-wave dgh-wave--2" d="M5.8 5.8a8.8 8.8 0 0 0 0 12.4M18.2 5.8a8.8 8.8 0 0 1 0 12.4" />
</svg>
<span>{signal}</span>
</p>
)
}
{
trustData && (
<div class="dgh-trust">
{trustData.avatars && trustData.avatars.length > 0 && (
<ul class="dgh-avatars">
{trustData.avatars.map((a) => (
<li class="dgh-avatar">
{a.src ? (
<img src={a.src} alt={a.alt ?? ""} width="34" height="34" loading="eager" decoding="async" />
) : (
<span class="dgh-avatar-ph" role="img" aria-label="Avatar placeholder: customer headshot, square 160 × 160 px" />
)}
</li>
))}
</ul>
)}
<div class="dgh-trust-copy">
{(trustData.title || trustData.stars) && (
<p class="dgh-trust-title">
{trustData.title}
{trustData.stars && <span class="dgh-stars" aria-hidden="true">★★★★★</span>}
</p>
)}
{trustData.text && <p class="dgh-trust-text">{trustData.text}</p>}
</div>
</div>
)
}
</div>
)}
</div>
{
showDash && dash && (
<div class="dgh-dash">
<span class="dgh-glow dgh-glow--lime" aria-hidden="true" />
<span class="dgh-glow dgh-glow--aqua" aria-hidden="true" />
<div class="dgh-dash-head">
{dash.eyebrow && (
<p class="dgh-dash-eyebrow">
<span class="dgh-dot dgh-dot--sm" aria-hidden="true" />
{dash.eyebrow}
</p>
)}
<svg class="dgh-pulse" viewBox="0 0 224 27" aria-hidden="true" focusable="false">
<path
class="dgh-pulse-line"
pathLength="1"
d="M1 15.5H58l4-6 4.5 11 4-16 4.5 18 3.5-7H116l3.5-4 3.5 8 4.5-12.5 4 14.5 3.5-6H176l4.5-8.5 4 11.5 3.5-3H219"
/>
<circle class="dgh-pulse-dot" cx="219" cy="15.5" r="2.6" />
</svg>
</div>
{dash.title && <h2 class="dgh-dash-title">{dash.title}</h2>}
{dash.stats && dash.stats.length > 0 && (
<ul class="dgh-stats">
{dash.stats.map((s, i) => (
<li class="dgh-stat" style={`--dgh-tone: ${s.color ?? TONES[i % TONES.length]};`}>
<span class="dgh-stat-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false" set:html={iconFor(s.icon, STAT_FALLBACK[i % 4])} />
</span>
<span class="dgh-stat-label">{s.label}</span>
<strong class="dgh-stat-value">{s.value}</strong>
{s.note && <span class="dgh-stat-note">{s.note}</span>}
</li>
))}
</ul>
)}
{dash.network && dash.network.length > 0 && (
<div class="dgh-net">
{dash.networkTitle && <h3 class="dgh-net-title">{dash.networkTitle}</h3>}
<ul class="dgh-net-grid">
{dash.network.map((n, i) => (
<li class="dgh-net-item" style={`--dgh-tone: ${n.color ?? TONES[i % TONES.length]};`}>
<svg class="dgh-net-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false" set:html={iconFor(n.icon, NET_FALLBACK[i % 4])} />
<strong class="dgh-net-value">{n.value}</strong>
<span class="dgh-net-label">{n.label}</span>
</li>
))}
</ul>
</div>
)}
{dash.insight &&
(dash.insight.detail && !dash.insight.href ? (
<details class="dgh-insight">
<summary class="dgh-insight-row">
<span class="dgh-insight-tag">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 3v5M12 16v5M3 12h5M16 12h5M6.3 6.3l2.5 2.5M15.2 15.2l2.5 2.5M6.3 17.7l2.5-2.5M15.2 8.8l2.5-2.5" />
</svg>
{dash.insight.label}
</span>
<span class="dgh-insight-text">{dash.insight.text}</span>
<span class="dgh-insight-chev" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false"><path d="M9.5 6l6 6-6 6" /></svg>
</span>
</summary>
<p class="dgh-insight-detail" id={insightId}>{dash.insight.detail}</p>
</details>
) : dash.insight.href ? (
<a class="dgh-insight-row dgh-insight-row--link" href={dash.insight.href}>
<span class="dgh-insight-tag">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M12 3v5M12 16v5M3 12h5M16 12h5M6.3 6.3l2.5 2.5M15.2 15.2l2.5 2.5M6.3 17.7l2.5-2.5M15.2 8.8l2.5-2.5" />
</svg>
{dash.insight.label}
</span>
<span class="dgh-insight-text">{dash.insight.text}</span>
<span class="dgh-insight-chev" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false"><path d="M9.5 6l6 6-6 6" /></svg>
</span>
</a>
) : (
<div class="dgh-insight-row dgh-insight-row--static">
<span class="dgh-insight-tag">{dash.insight.label}</span>
<span class="dgh-insight-text">{dash.insight.text}</span>
</div>
))}
</div>
)
}
</div>
</section>
)}
<style>
.dgh {
--dgh-ink: var(--nv-ink, #08110f);
--dgh-lime: var(--nv-lime, #c8ff38);
--dgh-text: #f7faf7;
--dgh-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
--dgh-ui: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
--dgh-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 800px;
padding: 96px 20px 56px;
overflow: hidden;
isolation: isolate;
background: var(--dgh-ink);
color: var(--dgh-text);
font-family: var(--dgh-font);
}
.dgh *,
.dgh *::before,
.dgh *::after {
box-sizing: border-box;
}
/* Zero-specificity resets, so every class below wins over them. */
:where(.dgh) :where(p, h1, h2, h3, ul) {
margin: 0;
}
:where(.dgh) :where(ul) {
padding: 0;
list-style: none;
}
/* ---- background ---- */
.dgh-bg {
position: absolute;
inset: 0;
z-index: -2;
}
.dgh-bg img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 70%;
}
/* The measured wash runs 0.25 → 0.08 left to right. The default dusk photo
is already dark, so the wash sits close to that: a little deeper behind
the copy column (0.42) for white type, near-clear on the right where the
glass dashboard carries its own contrast. See README → Approximations. */
.dgh-wash {
position: absolute;
inset: 0;
z-index: -1;
background:
linear-gradient(180deg, rgb(6 16 13 / 0.2) 0%, rgb(6 16 13 / 0) 26%, rgb(6 16 13 / 0) 70%, rgb(6 16 13 / 0.35) 100%),
linear-gradient(90deg, rgb(6 16 13 / 0.42) 0%, rgb(6 16 13 / 0.25) 38%, rgb(6 16 13 / 0.08) 100%);
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
}
.dgh-ph {
display: grid;
place-items: end;
width: 100%;
height: 100%;
padding: 20px 24px;
text-align: right;
background: repeating-linear-gradient(135deg, #1a2522 0 12px, #202d29 12px 24px);
color: rgb(247 250 247 / 0.6);
}
.dgh-ph-text {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
}
.dgh-ph-label {
font: 600 12px/1 var(--dgh-ui);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.dgh-ph-hint {
max-width: 30ch;
font: 500 14px/1.4 var(--dgh-ui);
}
.dgh-ph-spec {
font: 400 12px/1.2 var(--dgh-ui);
}
/* ---- layout ---- */
.dgh-inner {
display: flex;
flex-direction: column;
gap: 28px;
width: 100%;
max-width: 1310px;
margin: 0 auto;
}
.dgh-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 490px;
min-width: 0;
}
/* ---- copy ---- */
.dgh-eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 34px;
max-width: 100%;
padding: 7px 15px;
border-radius: 999px;
background: rgb(8 17 15 / 0.54);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
color: var(--dgh-lime);
font: 800 12px/14.4px var(--dgh-font);
letter-spacing: 0.72px;
text-transform: uppercase;
}
.dgh-dot {
flex: none;
width: 8px;
height: 8px;
border-radius: 999px;
background: var(--dgh-lime);
}
.dgh-dot--sm {
width: 6px;
height: 6px;
}
.dgh-title {
margin-top: 26px;
color: #fff;
font-family: var(--dgh-font);
font-weight: 800;
font-size: clamp(44px, 16.4vw, 62px);
line-height: 1.02;
letter-spacing: -0.055em;
}
.dgh-line {
display: block;
}
.dgh-line--accent {
color: var(--dgh-lime);
}
.dgh-punct {
color: #fff;
}
.dgh-body {
margin-top: 28px;
color: rgb(255 255 255 / 0.78);
font: 400 16px/24.8px var(--dgh-font);
}
.dgh-ctas {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
margin-top: 32px;
}
.dgh-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
min-height: 56px;
padding: 0 22px;
border-radius: 999px;
font: 800 15px/18px var(--dgh-font);
text-decoration: none;
transition:
transform 0.3s var(--dgh-ease),
background-color 0.3s var(--dgh-ease);
}
.dgh-btn--primary {
background: var(--dgh-lime);
color: var(--dgh-ink);
}
.dgh-btn--secondary {
background: rgb(16 27 24 / 0.92);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
color: #f7fafc;
}
.dgh-btn:hover {
transform: scale(1.1) translateY(-2px);
}
.dgh-btn:focus-visible {
outline: 2px solid var(--dgh-lime);
outline-offset: 3px;
}
.dgh-btn--primary:focus-visible {
outline-color: #fff;
}
.dgh-btn-icon {
flex: none;
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.dgh-btn-icon--sm {
width: 16px;
height: 16px;
}
.dgh-utility {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
width: 100%;
margin-top: 28px;
}
.dgh-labels {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 7px;
color: #fff;
font: 700 12px/14.4px var(--dgh-font);
letter-spacing: 0.6px;
text-transform: uppercase;
}
.dgh-labels li {
display: inline-flex;
align-items: center;
gap: 7px;
}
.dgh-labels li + li::before {
content: "";
width: 5px;
height: 5px;
border-radius: 999px;
background: var(--dgh-lime);
}
.dgh-signal {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 38px;
padding: 10px 14px;
border-radius: 999px;
background: rgb(8 17 15 / 0.66);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
color: #f7fafc;
font: 800 10px/12px var(--dgh-font);
letter-spacing: 0.7px;
text-transform: uppercase;
}
.dgh-signal svg {
flex: none;
width: 16px;
height: 16px;
color: var(--dgh-lime);
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
}
.dgh-trust {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
max-width: 410px;
min-height: 66px;
padding: 10px 12px;
border-radius: 18px;
background: rgb(8 17 15 / 0.58);
-webkit-backdrop-filter: blur(14px);
backdrop-filter: blur(14px);
}
.dgh-avatars {
display: flex;
flex: none;
gap: 8px;
}
.dgh-avatar img,
.dgh-avatar-ph {
display: block;
width: 34px;
height: 34px;
border-radius: 999px;
object-fit: cover;
}
.dgh-avatar-ph {
background: repeating-linear-gradient(135deg, #2b3934 0 5px, #34443e 5px 10px);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.18);
}
.dgh-trust-copy {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.dgh-trust-title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 7px;
color: #f7fafc;
font: 700 10px/12px var(--dgh-font);
letter-spacing: 0.5px;
text-transform: uppercase;
}
.dgh-stars {
color: var(--dgh-lime);
font: 700 9px/11px var(--dgh-ui);
letter-spacing: 1.32px;
}
.dgh-trust-text {
color: #f7fafc;
font: 600 11px/13.2px var(--dgh-font);
}
/* ---- dashboard ---- */
.dgh-dash {
position: relative;
display: none;
flex-direction: column;
width: 100%;
max-width: 668px;
min-height: 478px;
padding: 20px;
overflow: hidden;
isolation: isolate;
border: 0.67px solid rgb(255 255 255 / 0.18);
border-radius: 22px;
background-color: rgb(8 15 13 / 0.66);
background-image: linear-gradient(145deg, rgb(255 255 255 / 0.075), rgb(255 255 255 / 0.02));
box-shadow:
0 22px 63px rgb(0 0 0 / 0.42),
inset 0 1px 0 rgb(255 255 255 / 0.1),
inset 0 -1px 0 rgb(255 255 255 / 0.024);
-webkit-backdrop-filter: blur(18px) saturate(1.35);
backdrop-filter: blur(18px) saturate(1.35);
opacity: 0.98;
font-family: var(--dgh-ui);
}
.dgh-glow {
position: absolute;
z-index: -1;
border-radius: 50%;
pointer-events: none;
}
.dgh-glow--lime {
top: -153px;
right: -97px;
width: 252px;
height: 252px;
background: radial-gradient(circle, rgb(200 255 56 / 0.19), transparent 68%);
}
.dgh-glow--aqua {
bottom: -146px;
left: -62px;
width: 203px;
height: 203px;
background: radial-gradient(circle, rgb(115 245 223 / 0.13), transparent 70%);
}
.dgh-dash-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 17px;
min-height: 42px;
padding-bottom: 15px;
}
.dgh-dash-eyebrow {
display: flex;
align-items: center;
gap: 7px;
color: var(--dgh-lime);
font: 800 8px/1.2 var(--dgh-ui);
letter-spacing: 0.96px;
text-transform: uppercase;
white-space: nowrap;
}
.dgh-pulse {
flex: 0 1 224px;
min-width: 0;
height: 27px;
filter: drop-shadow(0 0 5px rgb(200 255 56 / 0.36));
}
.dgh-pulse-line {
fill: none;
stroke: var(--dgh-lime);
stroke-width: 1.3;
stroke-linejoin: round;
stroke-linecap: round;
}
.dgh-pulse-dot {
fill: var(--dgh-lime);
}
.dgh-dash-title {
color: var(--dgh-text);
font: 750 32px/32px var(--dgh-ui);
letter-spacing: -1.44px;
}
.dgh-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 18px;
border-top: 1px solid rgb(255 255 255 / 0.08);
border-bottom: 1px solid rgb(255 255 255 / 0.08);
}
.dgh-stat {
display: flex;
flex-direction: column;
align-items: center;
padding: 17px 13px 15px;
text-align: center;
}
.dgh-stat + .dgh-stat,
.dgh-net-item + .dgh-net-item {
border-left: 1px solid rgb(255 255 255 / 0.08);
}
.dgh-stat-icon {
display: grid;
place-items: center;
width: 45px;
height: 45px;
border: 0.67px solid rgb(255 255 255 / 0.11);
border-radius: 50%;
background: rgb(255 255 255 / 0.035);
box-shadow: inset 0 0 17px rgb(255 255 255 / 0.024);
color: var(--dgh-tone);
}
.dgh-stat-icon svg,
.dgh-net-icon {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
}
.dgh-stat-label {
display: flex;
align-items: center;
justify-content: center;
min-height: 24px;
margin-top: 12px;
color: rgb(247 250 247 / 0.62);
font: 750 8px/11.2px var(--dgh-ui);
letter-spacing: 0.64px;
text-transform: uppercase;
}
.dgh-stat-value {
margin-top: 6px;
color: var(--dgh-tone);
font: 820 20px/20px var(--dgh-ui);
letter-spacing: -0.7px;
}
.dgh-stat-note {
margin-top: 4px;
color: rgb(247 250 247 / 0.72);
font: 400 8px/10.8px var(--dgh-ui);
}
.dgh-net {
padding-top: 18px;
}
.dgh-net-title {
color: rgb(247 250 247 / 0.72);
font: 800 8px/10px var(--dgh-ui);
letter-spacing: 0.88px;
text-transform: uppercase;
}
.dgh-net-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
margin-top: 13px;
}
.dgh-net-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 13px 15px;
text-align: center;
}
.dgh-net-icon {
width: 24px;
height: 24px;
color: var(--dgh-tone);
}
.dgh-net-value {
margin-top: 10px;
color: var(--dgh-text);
font: 800 19px/19px var(--dgh-ui);
letter-spacing: -0.665px;
}
.dgh-net-label {
margin-top: 5px;
color: rgb(247 250 247 / 0.62);
font: 400 8px/10px var(--dgh-ui);
}
/* ---- forecast row ---- */
.dgh-insight {
margin-top: 0;
}
.dgh-insight-row {
display: grid;
grid-template-columns: auto minmax(0, 1fr) 24px;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 8px 10px 8px 8px;
border: 0.67px solid rgb(255 255 255 / 0.11);
border-radius: 999px;
background: rgb(255 255 255 / 0.035);
color: inherit;
text-decoration: none;
cursor: pointer;
list-style: none;
transition:
border-color 0.18s ease,
background-color 0.18s ease,
transform 0.18s ease;
}
.dgh-insight-row::-webkit-details-marker {
display: none;
}
.dgh-insight-row--static {
grid-template-columns: auto minmax(0, 1fr);
cursor: default;
}
.dgh-insight-row:not(.dgh-insight-row--static):hover {
background: rgb(255 255 255 / 0.07);
transform: translateX(1px);
}
.dgh-insight-row:focus-visible {
outline: 2px solid var(--dgh-lime);
outline-offset: 2px;
}
.dgh-insight-tag {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 24px;
padding: 0 8px;
border-radius: 999px;
background: rgb(200 255 56 / 0.08);
color: var(--dgh-lime);
font: 850 8px/1 var(--dgh-ui);
letter-spacing: 0.56px;
text-transform: uppercase;
white-space: nowrap;
}
.dgh-insight-tag svg {
width: 11px;
height: 11px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
}
.dgh-insight-text {
color: rgb(247 250 247 / 0.78);
font: 400 8px/11.6px var(--dgh-ui);
}
.dgh-insight-chev {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 50%;
color: var(--dgh-text);
transition: transform 0.18s ease;
}
.dgh-insight-chev svg {
width: 13px;
height: 13px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.dgh-insight[open] .dgh-insight-chev {
transform: rotate(90deg);
}
.dgh-insight-detail {
padding: 10px 16px 0;
color: rgb(247 250 247 / 0.72);
font: 400 10px/15px var(--dgh-ui);
}
/* ---- load-in ----
A CSS animation, not a script: it always finishes at rest, so without
JavaScript the content still ends visible, and nothing is hidden when
motion is reduced because the animation never applies. */
@media (prefers-reduced-motion: no-preference) {
.dgh--animate .dgh-copy {
animation: dgh-rise 0.5s var(--dgh-ease) both;
}
.dgh--animate .dgh-dash {
animation: dgh-rise 0.6s var(--dgh-ease) 0.12s both;
}
.dgh--animate .dgh-pulse-line {
stroke-dasharray: 1;
animation: dgh-draw 1.4s ease-out 0.35s both;
}
.dgh-wave--1 {
animation: dgh-wave 2.4s ease-in-out infinite;
}
.dgh-wave--2 {
animation: dgh-wave 2.4s ease-in-out 0.3s infinite;
}
}
@keyframes dgh-rise {
from {
opacity: 0;
transform: translateY(18px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes dgh-draw {
from {
stroke-dashoffset: 1;
}
to {
stroke-dashoffset: 0;
}
}
@keyframes dgh-wave {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.35;
}
}
@media (prefers-reduced-motion: reduce) {
.dgh-btn:hover,
.dgh-insight-row:not(.dgh-insight-row--static):hover {
transform: none;
}
.dgh-insight-chev {
transition: none;
}
}
/* Phone: the reference keeps only the first two labels on one line
rather than wrapping a third under them. */
@media (max-width: 809.98px) {
.dgh-labels li:nth-child(n + 3) {
display: none;
}
}
/* ---- tablet: dashboard stacks under the copy ---- */
@media (min-width: 810px) {
.dgh {
min-height: 860px;
padding: 88px 40px 92px;
}
.dgh-inner {
gap: 48px;
}
.dgh-title {
font-size: 66px;
line-height: 61.38px;
letter-spacing: -3.6px;
}
.dgh-ctas {
flex-direction: row;
flex-wrap: wrap;
gap: 16px;
width: auto;
}
.dgh-labels {
gap: 10px;
}
.dgh-labels li {
gap: 10px;
}
.dgh-dash {
display: flex;
}
}
/* ---- desktop: side by side ---- */
@media (min-width: 1200px) {
/* Measured: the copy starts 168px down and the dashboard 267px down
in an 860px hero. The overlay header occupies the top 78px. */
.dgh {
padding: 151px 64px 29px;
}
.dgh-inner {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 42px;
}
.dgh-copy {
flex: 0 0 490px;
}
.dgh-dash {
flex: 0 1 668px;
margin-top: 32px;
}
}
</style>
===== END src/components/DashboardGlassHero.astro =====
===== FILE (supporting): src/components/DashboardGlassHero.md =====
# Dashboard glass hero
An 860px full-bleed photo hero under a dark glass wash. Copy on the left:
pill eyebrow, four-line headline with alternate lime lines, paragraph, two
pill buttons, tracked labels, a network-signal pill and an avatar trust card.
On the right, a translucent operations dashboard: status title, four stat
cells, a live-network row and an expandable forecast row.
Catalog page: `/components/dashboard-glass-hero`
## Purpose
The opening section of a freight, logistics or operations-software homepage,
where the product is a control tower and the hero should look like one. It
sits directly under a ticker and leaves room at the top for an overlay header.
Use a simpler hero if you have no numbers worth showing: the dashboard only
earns its space when the stats are real.
## Files required
**`DashboardGlassHero.astro`**: one file, the whole component.
- No imports, no npm packages, no icon library, no framework.
- No script. Icons are inline SVG.
- Loads Manrope and Inter from Google Fonts unless `fontStylesheet=""`.
`DashboardGlassHero.md`: this file. Documentation, not required to run.
Photos (the catalog demo's, optional; see **Images and credits**):
`public/dashboard-glass-hero/desert-highway.webp`, `avatar-1.webp`,
`avatar-2.webp`, `avatar-3.webp`. The component has no default photo.
**The component ships no copy, figures or photo of its own.** Eyebrow,
headline, paragraph, buttons, labels, signal pill, background and every
dashboard field default to empty, so a copied hero never carries the catalog's
fictional control tower, its lane counts, its 98.6% or its forecast. The
catalog demo passes them through the registry's `props`. Each block is drawn
only when its data is present; a dashboard with nothing in it is not drawn;
and with no copy, link, proof or dashboard at all the hero renders nothing.
## Basic usage
```astro
---
import DashboardGlassHero from "../components/DashboardGlassHero.astro";
---
<DashboardGlassHero
headline={["Ship", "anything.", "See", "everything."]}
background={{ src: "/img/your-photo.webp", alt: "" }}
primaryCta={{ label: "Plan your shipment", href: "/quote" }}
secondaryCta={{ label: "Open route brief", href: "#platform" }}
dashboard={{
title: "All lanes running clear",
stats: [{ label: "On-time arrivals", value: "98.6%", icon: "clock" }],
}}
/>
```
No client directive and no wrapper. It renders the page's `<h1>`.
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `eyebrow` | `string?` | `""` | Pill above the headline. `""` drops it. |
| `headline` | `string[]?` | `[]` | One entry per line. Lines alternate white / lime, starting white. Trailing punctuation is drawn white. `[]` drops the `<h1>` (and the section's `aria-labelledby`). |
| `body` | `string?` | `""` | `""` drops it. |
| `primaryCta` | `{ label, href } \| null` | `null` | Lime pill. |
| `secondaryCta` | `{ label, href } \| null` | `null` | Dark glass pill. |
| `labels` | `string[]?` | `[]` | Tracked labels with lime dot separators. Only the first two show on a phone. |
| `signal` | `string?` | `""` | The signal pill. `""` drops it. |
| `trust` | `{ title?, text?, stars?, avatars? } \| null` | none — no card | Real social proof only; nothing in it has a default. Stars need an explicit `stars: true`. Each avatar is `{ src?, alt? }`; `src: ""` shows a placeholder circle. The catalog demo passes its card explicitly. |
| `background` | `{ src?, alt? }` | `{}` — none | Omitted, or `src: ""`, renders a generic labelled placeholder. |
| `dashboard` | object \| `null` | `{}` — none drawn | `null`, or a dashboard with no `eyebrow`, `title`, `stats`, `network` or `insight`, drops the dashboard. Keys below. Only what you pass is drawn: no figure is ever filled in. |
| `animate` | `boolean?` | `true` | The load-in rise and heartbeat draw. Off under reduced motion regardless. |
| `fontStylesheet` | `string?` | Manrope + Inter | `""` to skip. |
| `id` | `string?` | none | Root id; also seeds the heading and forecast ids. |
`dashboard` keys (each optional, none has a default except `networkTitle`):
| Key | Type | What it does |
| :--- | :--- | :--- |
| `eyebrow` | `string` | Lime label beside the heartbeat line. Omitted: no label. |
| `title` | `string` | The big status line (`<h2>`). Omitted: no status line. |
| `stats` | `{ label, value, note?, icon?, color? }[]` | The four stat cells. `icon`: `shield`, `clock`, `leaf`, `box`, `road`, `ship`, `plane`, `warehouse`. `color` is any CSS colour; defaults cycle lime, aqua, green, purple. |
| `networkTitle` | `string` | Label over the network row (`<h3>`). Defaults to the generic `"Live network"`; shown only with a `network` row. |
| `network` | `{ label, value, icon?, color? }[]` | The four network cells. |
| `insight` | `{ label, text, detail?, href? } \| null` | The forecast row. See below. |
## Behaviour
**Layout.** Side by side from 1200px (copy 490px, dashboard up to 668px,
space-between, dashboard 16px lower than centre as measured). From 810px to
1199px the dashboard stacks under the copy. Below 810px it is hidden and the
buttons go full width and stack, matching the reference phone capture.
**Hover** (all transitions ease-out, transforms off under reduced motion):
| Element | Change |
| :--- | :--- |
| Both hero buttons | `scale(1.1) translateY(-2px)`, 0.3s |
| Forecast row | background `rgba(255,255,255,.035)` → `.07`, `translateX(1px)`, 0.18s (the measured transition) |
**Animation.**
- Load-in: the copy column rises 18px and fades in over 0.5s; the dashboard
follows 0.12s later over 0.6s. A CSS animation with `both` fill, so it always
finishes at rest and needs no script.
- The heartbeat line draws itself once (1.4s) after the dashboard lands.
- The two arcs of the signal icon pulse softly (2.4s loop).
- Under `prefers-reduced-motion: reduce` none of these run.
**Forecast row.** With `detail` it is a `<details>`/`<summary>` disclosure:
the row is the summary, the chevron turns 90° when open, and the detail
sentence appears under it. A disclosure was chosen over a link because the
insight is a sentence of context, not a destination; keeping it in place
avoids a dead link in a demo. Pass `href` instead to make the row a link.
## Accessibility
- `<section aria-labelledby>` pointing at the `<h1>` (omitted when there is no headline).
- The headline is one `<h1>`; each line is a block `<span>`, so it reads as a
sentence.
- The forecast disclosure uses native `<summary>`: focusable, Enter/Space to
toggle, expanded state announced without ARIA.
- Visible `:focus-visible` rings (lime on dark, white on the lime button).
- Icons, dots, glows, stars and the heartbeat are `aria-hidden`. The trust
title already says the rating in words.
- Buttons are 56px tall; the forecast row is at least 44px.
- The dashboard's 8px labels are the measured size. They are small; the
dashboard is hidden on phones, where they would be hardest to read.
## Integration notes
- Leaves the top clear for an overlay header at top 14px: 88px padding up to
1199px, 151px at desktop.
- Renders the page's `<h1>`. Use it once per page.
- Theme with `--nv-ink`, `--nv-lime`, `--nv-font`, `--nv-font-ui` on any
ancestor. The component never sets them.
## Images and credits
| File | Slot | Size | Credit |
| :--- | :--- | :--- | :--- |
| `public/dashboard-glass-hero/desert-highway.webp` | Background: a mountain range at dusk under cloud, a highway with a guardrail along the bottom, a white truck cab lower right | 2000 × 1200 | Photo: Unsplash xJte6gX3mQE, Unsplash License |
| `public/dashboard-glass-hero/avatar-1.webp` | Trust card avatar 1 | 160 × 160 | Photo: Unsplash iFgRcqHznqg, Unsplash License |
| `public/dashboard-glass-hero/avatar-2.webp` | Trust card avatar 2 | 160 × 160 | Photo: Unsplash mEZ3PoFGs_k, Unsplash License |
| `public/dashboard-glass-hero/avatar-3.webp` | Trust card avatar 3 | 160 × 160 | Photo: Unsplash 7YVZYZeITc8, Unsplash License |
The background is framed with `object-position: center 70%` so the highway
and cab stay in view. It is decorative (`alt=""`). If you use a photo that
carries meaning, pass `background={{ src, alt: "…" }}`. The avatars are also
decorative, because the trust line beside them carries the meaning.
## Approximations
- **Overlay is slightly deeper than measured.** The reference wash is
`linear-gradient(90deg, rgba(6,16,13,.25) 33%, .08)`. This one runs
`.42 → .25 (38%) → .08` plus a light top (.2) and bottom (.35) vignette, so
white type stays legible over any photo swapped in. A much brighter photo
may need the left stop raised again.
- **Headline colour.** Measured lime on the headline is `#b8ff26` and on the
stars `#beff22`; both use the one `--nv-lime` token (`#c8ff38`).
- **Tablet.** No tablet capture exists. The stacked dashboard between 810 and
1199px is inferred.
- **Phone.** The reference crops its own headline at the top (the content is
taller than its fixed 800px). This version grows instead of cropping, and
sizes the headline `clamp(44px, 16.4vw, 62px)` so "everything." stays on one
line down to 320px. Line height is estimated from the screenshot.
- **Avatars** are spaced 8px apart as measured, not overlapping.
- **Stat cell separators** (1px, 8% white) are read off the screenshot; the
dump reports the borders as mixed.
- **Signal-wave pulse** is inferred: the pill was caught at opacity 0.74 in
the dump, which suggests a loop. The pulse is on the icon arcs only, so the
text keeps full contrast.
- **Heartbeat path** is an original drawing at the measured 224 × 27 box and
drop-shadow, not traced.
- **Icons** are original line drawings, not the reference set.
===== END src/components/DashboardGlassHero.md =====
===== FILE (required): public/dashboard-glass-hero/desert-highway.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/desert-highway.webp and save it to public/dashboard-glass-hero/desert-highway.webp]
===== FILE (required): public/dashboard-glass-hero/avatar-1.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-1.webp and save it to public/dashboard-glass-hero/avatar-1.webp]
===== FILE (required): public/dashboard-glass-hero/avatar-2.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-2.webp and save it to public/dashboard-glass-hero/avatar-2.webp]
===== FILE (required): public/dashboard-glass-hero/avatar-3.webp =====
[binary asset — download https://astro.baysixmedia.com/dashboard-glass-hero/avatar-3.webp and save it to public/dashboard-glass-hero/avatar-3.webp]
--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
No usable source fingerprint, so these results cannot be tied to the files below.
--- NOT PROVEN ---
- ClientRouter: not applicable — this component ships no script.
--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.
--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.