The 1 file 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: "Lime contact footer"
Library page: https://astro.baysixmedia.com/components/lime-contact-footer/
Files: 1 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/LimeContactFooter.astro =====
---
/**
* LimeContactFooter
*
* A dark green-black footer: logo tile, two-line wordmark, tagline and icon
* links on the left; Explore and Company link columns and a Contact column
* (phone, email, address, a lime pill) on the right; a hairline-boxed bottom
* bar with legal links and the copyright. Static markup, no script.
*
* Nothing about the business is assumed: no name, tagline, link, contact
* detail, pill or legal link is shown unless it is passed, a block with
* nothing in it is not drawn, and with no content at all nothing renders.
*
* Reads --nv-* tokens from any ancestor, with fallbacks.
*/
export interface LimeFooterLink {
label: string;
href: string;
}
export interface LimeFooterColumn {
heading: string;
links: LimeFooterLink[];
}
export type LimeFooterGlyph = "globe" | "play" | "chat" | "mail" | "phone" | "pin" | "rss" | "link";
export interface LimeFooterSocial {
/** Accessible name, e.g. "Halcyon on video". */
label: string;
href: string;
glyph: LimeFooterGlyph;
/** Icon colour. Each defaults to its own hue, as in the reference. */
color?: string;
}
export interface LimeFooterContact {
heading?: string;
phone?: string;
email?: string;
/** Lines of the postal address. */
address?: string[];
}
export interface LimeContactFooterProps {
/** First wordmark line. No name and no logo drops the logo link. */
brandName?: string;
/** Second, lime line of the wordmark. `""` drops it. */
brandSub?: string;
brandHref?: string;
/** Replaces the drawn logo tile and wordmark with an image. */
logo?: { src: string; alt: string; width?: number; height?: number };
tagline?: string;
social?: LimeFooterSocial[];
columns?: LimeFooterColumn[];
/** `null` drops the whole contact column. */
contact?: LimeFooterContact | null;
/** The lime pill under the contact details. `null` drops it. */
cta?: { label: string; href: string } | null;
legalLinks?: LimeFooterLink[];
/** Name in the copyright line. Defaults to the brand; with neither, no line. */
copyrightName?: string;
/** Defaults to the year the site was built. */
year?: number;
navLabel?: string;
fontStylesheet?: string;
id?: string;
}
interface Props extends LimeContactFooterProps {}
const {
brandName = "",
brandSub = "",
brandHref = "/",
logo,
tagline = "",
social = [],
columns = [],
contact = null,
cta = null,
legalLinks = [],
copyrightName,
year = new Date().getFullYear(),
navLabel = "Footer",
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;
/* A dialable href from a displayed number: keep a leading + and the digits. */
const telHref = (number: string) => {
const trimmed = number.trim();
const digits = trimmed.replace(/\D/g, "");
return digits ? `tel:${trimmed.startsWith("+") ? "+" : ""}${digits}` : "";
};
/* No name, no copyright line: never "Copyright 2026 ." */
const owner = copyrightName ?? (brandName ? (brandSub ? `${brandName} ${brandSub}` : brandName) : "");
/* A call link only with somewhere to dial: words in `phone` are not a number. */
const callHref = contact?.phone ? telHref(contact.phone) : "";
const hasDetails = Boolean(callHref || contact?.email || (contact?.address && contact.address.length > 0));
const showContact = Boolean(contact && (hasDetails || cta));
const hasMark = Boolean(logo || brandName);
const hasBrand = Boolean(hasMark || tagline || social.length > 0);
const hasCols = columns.length > 0 || showContact || Boolean(cta);
const hasBottom = legalLinks.length > 0 || Boolean(owner);
const hasContent = hasBrand || hasCols || hasBottom;
/* Generic 24px line glyphs, no third-party logos. */
const GLYPHS: Record<LimeFooterGlyph, string> = {
globe: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18Zm-9 9h18M12 3c2.5 2.6 3.8 5.6 3.8 9s-1.3 6.4-3.8 9c-2.5-2.6-3.8-5.6-3.8-9S9.5 5.6 12 3Z",
play: "M4.5 6.5A2.5 2.5 0 0 1 7 4h10a2.5 2.5 0 0 1 2.5 2.5v11A2.5 2.5 0 0 1 17 20H7a2.5 2.5 0 0 1-2.5-2.5v-11ZM10 9v6l5-3-5-3Z",
chat: "M4 5.5A1.5 1.5 0 0 1 5.5 4h13A1.5 1.5 0 0 1 20 5.5v9a1.5 1.5 0 0 1-1.5 1.5H10l-4.5 4v-4h0A1.5 1.5 0 0 1 4 14.5v-9ZM8 9h8M8 12h5",
mail: "M3.5 6.5A1.5 1.5 0 0 1 5 5h14a1.5 1.5 0 0 1 1.5 1.5v11A1.5 1.5 0 0 1 19 19H5a1.5 1.5 0 0 1-1.5-1.5v-11Zm.5.5 8 6 8-6",
phone: "M6.5 3.5h3l1.5 4-2 1.5a11 11 0 0 0 6 6l1.5-2 4 1.5v3a2 2 0 0 1-2 2A16.5 16.5 0 0 1 4.5 5.5a2 2 0 0 1 2-2Z",
pin: "M12 21s-6.5-5.7-6.5-11a6.5 6.5 0 0 1 13 0c0 5.3-6.5 11-6.5 11Zm0-8.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
rss: "M5 5a14 14 0 0 1 14 14M5 11a8 8 0 0 1 8 8M6 19a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
link: "M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1",
};
---
{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<footer class="lcf" id={id} data-lcf-root>
{(hasBrand || hasCols) && (
<div class="lcf-top">
{hasBrand && (
<div class="lcf-brand">
{hasMark && (
<a class="lcf-logo" href={brandHref} aria-label={logo ? undefined : `${brandName} home`}>
{
logo ? (
<img
class="lcf-logo-img"
src={logo.src}
alt={logo.alt}
width={logo.width ?? 190}
height={logo.height ?? 50}
/>
) : (
<>
<span class="lcf-tile" aria-hidden="true">
<svg viewBox="0 0 32 32" focusable="false">
<rect x="4" y="4" width="7" height="24" rx="2" fill="var(--nv-lime, #c8ff38)" />
<rect x="21" y="4" width="7" height="24" rx="2" fill="#fff" />
<path d="M11 18.5 21 10v6.2L11 24.7z" fill="var(--nv-lime, #c8ff38)" />
</svg>
</span>
<span class="lcf-wordmark" aria-hidden="true">
<span class="lcf-word">{brandName}</span>
{brandSub && <span class="lcf-sub">{brandSub}</span>}
</span>
</>
)
}
</a>
)}
{tagline && <p class="lcf-tagline">{tagline}</p>}
{
social.length > 0 && (
<ul class="lcf-social" role="list">
{social.map((s) => (
<li>
<a class="lcf-social-link" href={s.href} aria-label={s.label} style={s.color ? `--lcf-glyph:${s.color}` : undefined}>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d={GLYPHS[s.glyph]} fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</a>
</li>
))}
</ul>
)
}
</div>
)}
{hasCols && (
<nav class="lcf-cols" aria-label={navLabel}>
{
columns.map((col) => (
<div class="lcf-col">
<h2 class="lcf-heading">{col.heading}</h2>
<ul class="lcf-links" role="list">
{col.links.map((link) => (
<li>
<a class="lcf-link" href={link.href}>{link.label}</a>
</li>
))}
</ul>
</div>
))
}
{
contact && showContact && (
<div class="lcf-col lcf-col--contact">
{contact.heading && <h2 class="lcf-heading lcf-heading--lime">{contact.heading}</h2>}
{hasDetails && (
<address class="lcf-address">
{callHref && <a class="lcf-detail" href={callHref}>{contact.phone}</a>}
{contact.email && <a class="lcf-detail" href={`mailto:${contact.email}`}>{contact.email}</a>}
{contact.address && contact.address.length > 0 && (
<span class="lcf-postal">
{contact.address.map((line, i) => (
<>
{line}
{i < contact.address!.length - 1 && <br />}
</>
))}
</span>
)}
</address>
)}
{cta && <a class="lcf-cta" href={cta.href}>{cta.label}</a>}
</div>
)
}
{!showContact && cta && <div class="lcf-col lcf-col--contact"><a class="lcf-cta" href={cta.href}>{cta.label}</a></div>}
</nav>
)}
</div>
)}
{hasBottom && (
<div class="lcf-bottom">
{
legalLinks.length > 0 && (
<nav class="lcf-legal" aria-label="Legal">
<ul role="list">
{legalLinks.map((link) => (
<li>
<a class="lcf-legal-link" href={link.href}>{link.label}</a>
</li>
))}
</ul>
</nav>
)
}
{owner && <p class="lcf-copy">Copyright {year} {owner}.</p>}
</div>
)}
</footer>
)}
<style>
.lcf {
--lcf-ink: var(--nv-ink, #08110f);
--lcf-lime: var(--nv-lime, #c8ff38);
--lcf-font: var(--nv-font, Manrope, ui-sans-serif, system-ui, sans-serif);
--lcf-font-ui: var(--nv-font-ui, Inter, ui-sans-serif, system-ui, sans-serif);
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 40px;
width: 100%;
padding: 64px 20px 28px;
overflow: hidden;
background: linear-gradient(135deg, #07100d 0%, #08150f 62%, #10241a 100%);
color: #fff;
font-family: var(--lcf-font);
}
.lcf *,
.lcf *::before,
.lcf *::after {
box-sizing: border-box;
}
.lcf ul {
margin: 0;
padding: 0;
list-style: none;
}
.lcf p,
.lcf h2 {
margin: 0;
}
/* Listed by class rather than `.lcf a`: Astro's scoping attribute would
give a descendant `a` rule more weight than each link's own colour. */
.lcf-logo,
.lcf-social-link,
.lcf-link,
.lcf-detail,
.lcf-legal-link,
.lcf-cta {
text-decoration: none;
}
.lcf-logo {
color: #fff;
}
/* ---- top: brand + columns ---- */
.lcf-top {
display: flex;
flex-direction: column;
gap: 28px;
width: 100%;
max-width: 1180px;
padding: 36px 0 28px;
}
.lcf-brand {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
max-width: 350px;
}
.lcf-logo {
display: inline-flex;
align-items: center;
gap: 10px;
border-radius: 8px;
}
.lcf-tile {
display: grid;
place-items: center;
width: 50px;
height: 50px;
flex: none;
border-radius: 8px;
background: rgb(255 255 255 / 0.06);
}
.lcf-tile svg {
width: 38px;
height: 38px;
}
.lcf-logo-img {
display: block;
height: 50px;
width: auto;
}
.lcf-wordmark {
display: flex;
flex-direction: column;
font-family: var(--lcf-font-ui);
font-weight: 700;
text-transform: uppercase;
}
.lcf-word {
font-size: 25px;
line-height: 30px;
letter-spacing: 1.75px;
color: #fff;
}
.lcf-sub {
font-size: 17px;
line-height: 20.4px;
letter-spacing: 4.08px;
color: #b7ff39;
}
.lcf-tagline {
font-size: 19px;
font-weight: 500;
line-height: 27.55px;
color: rgb(255 255 255 / 0.66);
}
/* 24px glyphs in 44px targets. The row pulls itself back by the target
padding so the first glyph still lines up with the tagline. */
.lcf .lcf-social {
display: flex;
align-items: center;
margin: -6px 0 -6px -10px;
}
.lcf-social-link {
display: grid;
place-items: center;
width: 44px;
height: 44px;
border-radius: 10px;
color: var(--lcf-glyph, rgb(255 255 255 / 0.78));
}
.lcf-social-link svg {
width: 24px;
height: 24px;
}
/* ---- columns ---- */
.lcf-cols {
display: grid;
grid-template-columns: repeat(2, minmax(0, 155px));
gap: 26px 20px;
padding: 10px;
}
.lcf-col {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
min-width: 0;
}
.lcf-heading {
font-size: 12px;
font-weight: 800;
line-height: 14.4px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: rgb(255 255 255 / 0.48);
}
.lcf-heading--lime {
letter-spacing: 0.075em;
color: rgb(200 255 56 / 0.78);
}
/* 16px type on the measured 35px row pitch. Each row's whole pitch is
its target, with no dead space between rows; the list pulls back by
half the gap so the first label sits the measured 14px under the
heading. */
.lcf .lcf-links {
display: flex;
flex-direction: column;
margin: -7px 0;
}
.lcf-link {
display: inline-flex;
align-items: center;
min-height: 35px;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
line-height: 20.8px;
color: rgb(255 255 255 / 0.84);
transition: color 0.25s ease;
}
.lcf-col--contact {
gap: 9px;
grid-column: 1;
}
.lcf-address {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 9px;
font-style: normal;
font-size: 12px;
font-weight: 600;
line-height: 14.4px;
color: rgb(255 255 255 / 0.78);
}
.lcf-detail {
display: inline-flex;
align-items: center;
min-height: 24px;
margin: -5px 0;
color: inherit;
border-radius: 4px;
transition: color 0.25s ease;
}
.lcf-postal {
line-height: 17.4px;
}
.lcf-cta {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 165px;
height: 40px;
margin-top: 9px;
padding: 0 18px;
border-radius: 999px;
background: var(--lcf-lime);
color: var(--lcf-ink);
font-size: 12px;
font-weight: 800;
line-height: 14.4px;
white-space: nowrap;
}
/* ---- bottom bar ---- */
.lcf-bottom {
display: flex;
flex-direction: column;
gap: 14px;
width: calc(100% + 40px);
max-width: none;
margin: 0 -20px;
padding: 18px 20px 28px;
border-top: 1px solid rgb(255 255 255 / 0.08);
border-bottom: 1px solid rgb(255 255 255 / 0.08);
}
.lcf .lcf-legal ul {
display: grid;
grid-template-columns: repeat(2, minmax(0, 160px));
gap: 10px;
padding: 10px;
}
.lcf-legal-link {
display: inline-flex;
align-items: center;
min-height: 24px;
margin: -4px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
line-height: 15.6px;
color: rgb(255 255 255 / 0.62);
transition: color 0.25s ease;
}
.lcf-copy {
font-size: 12px;
font-weight: 500;
line-height: 15.6px;
color: rgb(255 255 255 / 0.5);
}
/* ---- links: no hover was measured. A quiet brighten on hover (an
approximation) and a clear lime ring plus full white on focus. ---- */
@media (hover: hover) {
.lcf-link:hover,
.lcf-detail:hover,
.lcf-legal-link:hover {
color: #fff;
}
}
.lcf a:focus-visible {
outline: 2px solid var(--lcf-lime);
outline-offset: 3px;
color: #fff;
}
.lcf .lcf-cta:focus-visible {
outline-color: #fff;
color: var(--lcf-ink);
}
/* ---- tablet: brand above, three columns in one row ---- */
@media (min-width: 810px) {
.lcf-cols {
display: flex;
align-items: flex-start;
gap: 56px;
}
.lcf-col {
width: 132px;
}
.lcf-col--contact {
width: 190px;
}
.lcf-bottom {
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1180px;
margin: 0;
/* 12px, not the 18px Framer reports: its 24px legal row carries
10px of padding the box does not grow by, and the measured box
is 60px tall. */
padding: 12px 10px 12px 0;
border: 1px solid rgb(255 255 255 / 0.08);
}
.lcf .lcf-legal ul {
display: flex;
align-items: center;
gap: 18px;
}
}
/* ---- desktop: brand left, columns right, as measured ---- */
@media (min-width: 960px) {
.lcf-top {
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
gap: 40px;
padding: 38px 0 30px;
}
.lcf-brand {
width: 220px;
flex: none;
}
.lcf-cols {
gap: 68px;
flex: none;
}
}
@media (min-width: 960px) and (max-width: 1099.98px) {
.lcf-cols {
gap: 44px;
}
}
@media (prefers-reduced-motion: reduce) {
.lcf a {
transition: none;
}
}
@media (forced-colors: active) {
.lcf-cta,
.lcf-tile {
border: 1px solid CanvasText;
}
}
</style>
===== END src/components/LimeContactFooter.astro =====
===== FILE (supporting): src/components/LimeContactFooter.md =====
# Lime contact footer
A dark green-black footer: logo tile, two-line wordmark, tagline and icon
links on the left; Explore and Company link columns and a Contact column
(phone, email, address and a lime pill) on the right; a hairline-boxed bottom
bar with legal links and the copyright.
Catalog page: `/components/lime-contact-footer`
## Purpose
The site footer for the Halcyon/Naviora-style page: navigation repeated,
direct contact details, one call to action. Static — no script.
## Files required
**`LimeContactFooter.astro`** — one file. No imports, no script, no npm
packages, no images.
`LimeContactFooter.md` — this file. Documentation, not required to run.
## Basic usage
```astro
---
import LimeContactFooter from "../components/LimeContactFooter.astro";
---
<LimeContactFooter
brandName="Halcyon"
brandSub="Logistics"
tagline="Freight that moves on time, and data that shows you it did."
columns={[{ heading: "Company", links: [{ label: "About", href: "/about" }, { label: "Contact", href: "/contact" }] }]}
contact={{ heading: "Contact", phone: "+1 (415) 555-0188", email: "desk@example.com", address: ["48 Harbor Point Road,", "Oakland, CA 94607, USA"] }}
cta={{ label: "Talk to dispatch", href: "/contact" }}
legalLinks={[{ label: "Privacy", href: "/privacy" }]}
/>
```
## Props
| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `brandName` | `string` | — | First wordmark line; home link's accessible name. No name and no `logo` drops the logo link. |
| `brandSub` | `string` | — | Lime second line. |
| `brandHref` | `string` | `"/"` | |
| `logo` | `{ src, alt, width?, height? }` | — | Replaces the tile and wordmark with an image (50px tall). |
| `tagline` | `string` | — | |
| `social` | `{ label, href, glyph, color? }[]` | `[]` | Icon links. `glyph`: globe, play, chat, mail, phone, pin, rss, link. Empty drops the row. |
| `columns` | `{ heading, links: { label, href }[] }[]` | `[]` | Link columns. |
| `contact` | `{ heading?, phone?, email?, address?: string[] } \| null` | `null` | The contact column. Phone becomes `tel:` (no digits, no line), email `mailto:`. |
| `cta` | `{ label, href } \| null` | `null` | The lime pill. |
| `legalLinks` | `{ label, href }[]` | `[]` | Empty drops the legal nav. |
| `copyrightName` | `string` | brand + sub | Name in the copyright line. No name and no brand: no copyright line. |
| `year` | `number` | build year | |
| `navLabel` | `string` | `"Footer"` | `aria-label` of the column nav. |
| `fontStylesheet` | `string` | Manrope + Inter on Google Fonts | `""` skips the `<link>`. |
| `id` | `string` | — | Root id. |
## Business details
Nothing about the business is built in. With no props the footer renders
nothing; each block (logo link, tagline, icon row, columns, contact column,
pill, legal nav, copyright, the bottom bar itself) appears only when it has
data. The catalog preview's Halcyon content is demo data from the registry,
not a default.
## Behaviour
- **Desktop (960px+).** 1180px wide, brand block (220px) left, three columns
right with 68px gaps (132 / 132 / 190px), 64px top padding, bottom bar in a
1px `rgba(255,255,255,.08)` box. Background
`linear-gradient(135deg, #07100d, #08150f 62%, #10241a)`.
- **Tablet (810–959px).** Brand block above, the three columns in one row.
- **Phone (<810px).** Brand block, then a 2 × 155px grid (Explore, Company,
Contact under Explore), then legal links in a 2 × 160px grid and the
copyright; the bottom bar's hairlines run edge to edge.
- **Hover.** None measured on links. A quiet brighten to full white on hover
(hover-capable pointers only) is this library's approximation. Focus
shows a 2px lime ring and full white text. The pill has no hover.
- **Motion.** Colour transitions only (0.25s); removed under reduced motion.
## Accessibility
- `<footer>`; the columns are a `<nav aria-label="Footer">` with `<h2>`
headings; legal links a second `<nav aria-label="Legal">`; contact details
in `<address>`.
- Icon links have `aria-label`s; glyphs are `aria-hidden`. Each is a 44px
target.
- Column links fill the measured 35px row pitch as their target, with no dead
space between rows. Contact and legal links are 24px targets (WCAG 2.5.8).
- Pill text is ink on lime.
## Integration notes
- Goes at the end of the layout, after `<main>`, as a direct child of
`<body>` so it is the page's contentinfo landmark.
- Paints its own full-bleed background; no wrapper.
- The default copyright year is the build year. A long-lived static deploy
shows a stale year; pass `year`.
- Ships no third-party logos: the icons are generic glyphs. Add paths to the
`GLYPHS` map in your copy for real network marks.
- The column headings are `<h2>`. If your page outline needs another level,
change the tag in your copy.
## Images and credits
None. The logo tile is an original inline SVG.
## Approximations
- **Pill text colour.** The reference's "Get in Touch" is `#0099ff` blue on
lime (a Framer default link colour, about 2.6:1). It is ink here.
- **Social icons.** The reference shows four real network logos in brand
colours. These are generic glyphs, each keeping a similar hue (blue, grey,
blue, red), and each sits in a 44px target, so the row is 44px apart
rather than the reference's 34px.
- **Link weights.** The reference mixes 500/84% and 600/78% links (plain text
versus links in Framer). All links here are 500/84%.
- **Bottom bar.** The box border is read from the screenshot (the dump
reports none), and the copyright gets 10px right padding to mirror the left.
- Tablet (810–1199px) was not captured; the 810–959px layout is interpolated.
===== END src/components/LimeContactFooter.md =====
--- 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.