Overview#
Compass is a website template for coaches and consultants. The demo content is for Bearing, the fictional
leadership coaching practice of Nadia Whitcombe in Pittsburgh, who works with new managers, founders and senior
leaders through 1:1 coaching, a group cohort and team workshops. The design is warm and editorial: Source Serif 4
headings with italic accents, Instrument Sans body text, an ivory, dusk-indigo and marigold palette and cut-paper SVG
illustrations. This is the Nuxt version: Nuxt 4, Vue 3, TypeScript and Tailwind CSS v4, generated as a fully static
site, with no other dependencies. This guide covers version 1.0.0 (see template.json). Live demo:
freeframe.dev/templates/compass.
Quick start#
Requirements: Node.js 24 and pnpm.
pnpm install
pnpm devOpen http://localhost:3000. Pages reload as you edit. Other scripts:
| Script | What it does |
|---|---|
pnpm generate |
Static site in .output/public/ |
pnpm build |
Node.js server build in .output/ (pages still prerendered) |
pnpm preview |
Serve the last build locally |
pnpm lint |
ESLint (flat config from @nuxt/eslint) |
pnpm typecheck |
nuxt prepare and vue-tsc |
Pages#
| Route | File | Purpose and notable features |
|---|---|---|
/ |
app/pages/index.vue |
Home: hero with portrait and intro-call CTA, press wordmarks, "who it's for" cards, count-up outcomes, programs preview, the four-step method, testimonial carousel, free guide sign-up, final CTA |
/programs |
app/pages/programs.vue |
Foundations and Executive 1:1 packages, strategy session, First 90 Days cohort, team workshops, comparison table, FAQ accordion, "Recommended by your self-check" badges |
/method |
app/pages/method.vue |
Interactive compass diagram with tabs, 60-minute session structure, six-month engagement timeline |
/results |
app/pages/results.vue |
Summary stats, eight client stories with before/after metrics and filters by client type (?type=), pull quote |
/about |
app/pages/about.vue |
Story, studio illustration, career path, credentials, values, personal notes |
/resources |
app/pages/resources.vue |
Leadership self-check quiz, resource library with topic filters (?topic=), podcast, guide sign-up |
/article |
app/pages/article.vue |
Sample long-form article with drop cap, illustration, pull quote, "try this week" box, author card, related links |
/book |
app/pages/book.vue |
Four-step booking flow with a live summary sidebar (?type=strategy or ?type=team preselects a call type) |
/contact |
app/pages/contact.vue |
SVG map, studio address, contact cards, validated contact form, other ways to work together |
| 404 | app/error.vue |
Wandering compass needle and links back into the site (generated as 404.html) |
Project structure#
compass/
app/
app.vue <NuxtLayout><NuxtPage />, keyed so a link to the current page starts it afresh
error.vue 404 page
router.options.ts keeps query strings written like the HTML version (?type=founders)
layouts/default.vue skip link, header, page, footer, back-to-top button
pages/ one .vue file per page (see the table above)
components/
SiteHeader.vue sticky header, main and mobile menus
SiteFooter.vue footer columns, studio address, the large Bearing wordmark, copyright year
BrandMark.vue the compass logo mark
LineIcon.vue draws an icon from app/data/icons.ts
BackToTop.vue back-to-top button
CountUp.vue count-up outcome numbers
CohortDate.vue First 90 Days start dates (rolling in the demo)
TestimonialCarousel.vue home page carousel
FaqList.vue programs FAQ accordion
RecommendedBadge.vue "Recommended by your self-check" badge
MethodCompass.vue method diagram and tabs
StoriesBrowser.vue client stories with filters
ResourceLibrary.vue resource cards with topic filters
SelfCheckQuiz.vue leadership self-check
BookingFlow.vue four-step booking flow and calendar
GuideForm.vue free guide sign-up (home page and resources)
ContactForm.vue contact form
FieldError.vue a field's inline error message
composables/
useValidation.ts inline validation for every form
useChipFilter.ts shared filter state and ?type= / ?topic= URLs
usePageSeo.ts per-page title, description, canonical URL and Open Graph tags
useCurrentPage.ts aria-current for menu links
useAsset.ts prefixes public/ files with the base path
useYear.ts the copyright year
data/
site.ts NAV and MOBILE_NAV menu items
programs.ts PROGRAMS (quiz result), COHORT_DATES and COHORT_MIN_WEEKS
quiz.ts QUIZ_KEY, QUIZ_QUESTIONS, QUIZ_RESULTS
booking.ts CALL_TYPES and AVAILABILITY
stories.ts STORY_FILTERS and STORIES (results page)
resources.ts RESOURCE_FILTERS and RESOURCES (library)
testimonials.ts TESTIMONIALS (home page carousel)
faq.ts FAQ (programs page)
method.ts METHOD_POINTS (method page tabs)
icons.ts ICONS: the line icon set
utils/
booking.ts date, time-zone and slot math, .ics file
quiz.ts scoring, reading and saving the self-check
cohort.ts cohort start dates
safeStorage.ts localStorage with an in-memory fallback
makeRef.ts short references like BL-7K2Q
plugins/
reveal.client.ts scroll reveals
same-page.client.ts links to the page already showing start it again at the top
stable-build.server.ts reproducible builds
assets/css/main.css Tailwind entry: fonts, @theme tokens and component classes
assets/fonts/ woff2 files and their license (OFL.txt)
public/img/ nadia-portrait.svg, studio.svg, guide-cover.svg, favicon.svg, og-image.png and og-image.svg
nuxt.config.ts base path, head defaults, SITE_URL, prerendered routes, Tailwind
template.json gallery metadata for freeframe.devCustomizing#
Brand and colors#
All design tokens live in the @theme block of app/assets/css/main.css. Each token becomes Tailwind utilities
(bg-dusk-800, text-marigold-700, border-linen-200 and so on).
| Token group | Used for |
|---|---|
--color-ivory, --color-linen-50 to --color-linen-300 |
Page background, soft section backgrounds, borders |
--color-stone-500 to --color-stone-800 |
Body copy and muted text |
--color-dusk-50 to --color-dusk-950 |
The brand indigo: buttons, links, dark sections, the footer |
--color-marigold-100 to --color-marigold-700 |
The warm accent: highlights, stars, accent buttons on dark backgrounds |
--color-clay-100 to --color-clay-700 |
Secondary accent for tags and illustrations |
To change the brand color, replace the dusk scale:
@theme {
--color-dusk-700: #2f5d50;
--color-dusk-800: #244a40;
--color-dusk-900: #1a3730;
--color-dusk-950: #12261f;
}Keep the contrast rules noted in the comments: use marigold-700 (not 400/500) for marigold text on light
backgrounds, and stone-600 or darker for small text. Reusable pieces such as .btn, .btn-primary, .card,
.chip, .input, .eyebrow and .display are defined in the @layer components block of the same file.
The illustrations (public/img/*.svg), the logo (app/components/BrandMark.vue) and the inline SVGs in the pages use
hex colors directly. If you change the palette, search for the old hex values (for example #2a3259 and #f0c464)
and replace them. The theme-color meta tag in nuxt.config.ts sets the browser bar color.
Typography#
Two variable fonts are self-hosted from app/assets/fonts/ with @font-face rules at the top of main.css. They are
the same woff2 files Google Fonts serves to the HTML version, so text measures exactly the same:
| Family | Token | Used for |
|---|---|---|
| Source Serif 4 (weights 300 to 600, roman and italic, optical sizes) | --font-serif |
Headings, numbers and quotes, through .display and font-serif |
| Instrument Sans (weights 400 to 700) | --font-sans |
Body text, buttons and labels |
To swap a font, put the new woff2 files in app/assets/fonts/, change the @font-face rules and the --font-*
token. Inline SVGs that contain text (press wordmarks, the method compass, the podcast cover, the map and the footer
wordmark) name the fonts too; quote family names that contain digits, as in
font-family="'Source Serif 4', Georgia, serif".
Content#
Page copy is markup in each page's <template>. Repeated and interactive content lives in app/data/:
| File | What it holds |
|---|---|
programs.ts |
PROGRAMS: name, price, link and summary of each program, used by the quiz result; COHORT_DATES and COHORT_MIN_WEEKS: cohort start dates (rolling in the demo, see below) |
quiz.ts |
QUIZ_QUESTIONS, QUIZ_RESULTS and the storage key QUIZ_KEY |
booking.ts |
CALL_TYPES (label, minutes, price, where) and AVAILABILITY (time zone, weekly hours, slot spacing, notice, booking window, closed dates) |
stories.ts |
STORY_FILTERS (the chips) and STORIES (client stories with metrics) |
resources.ts |
RESOURCE_FILTERS and RESOURCES (library cards) |
testimonials.ts |
TESTIMONIALS for the home page carousel |
faq.ts |
FAQ questions and answers on the programs page |
method.ts |
METHOD_POINTS: the four method tabs and their panels |
site.ts |
The NAV and MOBILE_NAV menus |
To add a client story, add an object to STORIES:
{
id: "maya",
tags: ["founders"],
labels: [{ label: "Founder", className: "tag tag-marigold" }],
program: "Executive 1:1 · 6 months",
title: "Your headline",
text: "Two or three sentences about what changed.",
metrics: [
{ label: "Meetings per week", before: "30", after: "18", bars: [100, 60] },
{ label: "Direct reports", before: "9", after: "4", bars: [100, 44] },
],
person: { initials: "MK", avatar: "bg-dusk-100 text-dusk-800", name: "Maya Kerr", role: "Founder, Example Co" },
},tags must be values from STORY_FILTERS (managers, founders, executives, teams, career); a story can
have several. The chip counts are worked out from the data. bars are the widths in percent of the before and after
bars (the after bar is indigo when it is longer, marigold when shorter); leave them out to show only the numbers. Two
metrics give a two-column row, three a three-column row. Resources work the same way with a single topic. To add a
filter, add a { value, label } to the filters array and use that value in some items.
Programs and prices appear in app/pages/programs.vue, in the home page preview, in the comparison table and in
PROGRAMS; keep them in step when you change a price.
Cohort dates. First 90 Days cohorts start on the second Wednesday of January, April, July and October.
COHORT_DATES in app/data/programs.ts is null in the demo, which shows rolling dates: the first start at least
COHORT_MIN_WEEKS (6) weeks away and the one after it, so the demo never advertises a cohort that has started. Set
{ next: "2027-01-13", after: "2027-04-14" } for your real dates. <CohortDate> (logic in app/utils/cohort.ts)
shows them on the home page and the programs page:
<CohortDate format="long" fallback="January 13, 2027" />
<CohortDate which="after" format="month" fallback="April 2027" />format is full (Wednesday, January 13, 2027), long, month (April 2027) or season (Winter 2027). The
generated HTML shows fallback and the browser shows the computed date after the page loads, so there is no hydration
mismatch.
Images and artwork#
All artwork is SVG, so there are no photos to license. To use real photos instead:
- Portrait (
public/img/nadia-portrait.svg, on home, about, article and book pages): replace with a 4:5 photo. To keep the arch shape, addarch object-coverto the<img>class;.archrounds the top into a full arch. - Studio (
public/img/studio.svgon about): any 10:7 landscape image. - Guide cover (
public/img/guide-cover.svg): a 360 x 460 (roughly 4:5) cover of your own lead magnet. - The method compass (
MethodCompass.vue), press wordmarks (home page), map (contact), podcast cover (resources), article illustration and the 404 compass are inline SVG. Replace the press wordmarks with your own logos, and only list publications that have really featured you.
Images in public/ are referenced with asset("img/...") (useAsset()) so they keep working under a base path. The
social image is public/img/og-image.png (1200 x 630), rendered from public/img/og-image.svg; edit the SVG and
export a new PNG of the same size. The favicon is public/img/favicon.svg.
Navigation, header and footer#
The header lives in app/components/SiteHeader.vue and reads its links from NAV in app/data/site.ts; the mobile
menu uses MOBILE_NAV, which adds Home first and "Book a free intro call" last. Each item's match lists the routes
that mark it as the current page with aria-current="page" (the article belongs to Resources). The desktop menu
appears from the lg breakpoint (1024 px); below that the menu button opens the mobile menu. The footer is
app/components/SiteFooter.vue. To add a page, create app/pages/<route>.vue with a usePageSeo() call, add the
route to routes in nuxt.config.ts and link it from NAV and the footer.
Features#
Booking flow#
/book renders app/components/BookingFlow.vue, a four-step flow:
-
Call type: radio cards, one per key in
CALL_TYPES. Link to/book?type=strategy(orteam) to preselect a type; unknown values are ignored. -
Date and time: a month calendar (Monday first) and a list of time slots. Slots come from
slotsFor()inapp/utils/booking.ts, which appliesAVAILABILITY:tsexport const AVAILABILITY = { timeZone: "America/New_York", zoneName: "Eastern Time", hours: { 1: [["09:00", "12:00"], ["13:30", "17:00"]], /* ... */ 5: [["09:00", "12:30"]] }, slotStep: 30, minNoticeHours: 24, daysAhead: 45, closed: ["2026-11-26", "2026-12-25" /* ... */], demoBusyShare: 0.35, };hoursis keyed by weekday (0 is Sunday) in studio time; a slot fits when its whole length (minutesof the call type) ends before the window closes.demoBusySharehides a stable share of slots so the demo looks realistic; set it to0once you connect real availability. Times are shown in the visitor's time zone (detected withIntl), with the studio time underneath and a "Show times in" selector to switch to studio time. Slots that cross midnight are labeled(+1 day). -
Details: a validated form (
[data-booking-form]) with name, email, role, optional company, a message of at least 20 characters and a consent checkbox. Back keeps the chosen day and time. -
Confirm: a summary of the request with a reference (see Forms) and how to change or cancel, a Save a calendar hold (.ics) button that downloads an event file built in the browser (
icsFor()), and Book another call.
The sidebar summary updates as choices are made. The calendar depends on today's date and the visitor's time zone, so
it is worked out in the browser (onMounted); the generated HTML shows step 1.
Accessibility: each step's heading receives focus when the step changes, and the step list marks the current step
with aria-current="step". Calendar days are buttons with full-date aria-labels that include the number of open
times; only one day is in the tab order, the arrow keys move by day and week, Home/End go to the start/end of the
week and Page Up/Page Down move four weeks. Unavailable days use aria-disabled="true". Time slots are native radio
buttons inside a fieldset, and a missing date or time shows an inline role="alert" message and moves focus back to
the calendar or the times.
Leadership self-check#
SelfCheckQuiz.vue on /resources#quiz has three views: intro, questions and result. Questions come from
QUIZ_QUESTIONS, one at a time as a fieldset of radio buttons with a progress bar and Back/Next buttons; choosing
nothing shows an inline message. Each answer is tagged with a result type; scoreQuiz() in app/utils/quiz.ts
picks the type with the most answers, and ties go to the type listed first in QUIZ_RESULTS. The result shows the
type, three steps to try, a bar for each type and the recommended program from PROGRAMS.
To change the quiz, edit the arrays: every question needs one answer per result type you want it to count toward,
and each result's program must be a key of PROGRAMS. Link to /resources#quiz from anywhere to send people to
it. The legend of each new question receives focus so screen reader users hear it, and the result heading receives
focus when it appears.
Recommended program badge#
On /programs, each program has a <RecommendedBadge program="..." />. It stays hidden unless the self-check result
saved on this device recommends that program (foundations, executive, cohort or teams). Classes set on the
component are passed to its <p>.
Method compass#
MethodCompass.vue on /method shows four tabs (standard role="tablist" with arrow-key, Home and End support) and
one panel each, from METHOD_POINTS. The compass needle turns to the selected point by the shortest route and the
matching point is highlighted; clicking a point in the drawing selects and focuses its tab. The drawing is decorative
for assistive technology, which uses the tabs.
Filters#
StoriesBrowser.vue (/results) and ResourceLibrary.vue (/resources) use useChipFilter(): chip buttons with
aria-pressed (all means everything), a live result count and an empty message. The selected filter is written to
the URL (?type= on results, ?topic= on resources) with history.replaceState when a chip is picked, so filtered
views can be shared and linked, for example /results?type=founders. Unknown values show everything. The URL is read
after the page loads, so the generated HTML shows every item.
Testimonial carousel#
TestimonialCarousel.vue is a horizontal scroll-snap list, so swiping, trackpads and the arrow keys (once the list is
focused) all work. The Previous/Next buttons scroll by one card and disable at the ends; the "1 / 6" counter is
visual only. There is no autoplay. Slides and their "1 of 6" labels come from TESTIMONIALS.
FAQ accordion#
FaqList.vue renders FAQ: each question is a .acc-btn button with aria-expanded and aria-controls pointing to
its answer. Set open: true on an item to open it by default; link adds a link at the end of the answer.
Count-up stats, scroll reveals and motion#
<CountUp :value="340" /> renders <span data-count="340">340</span> (add :decimals="1" for 6.5) and counts up
once when it scrolls into view; the final value is already in the generated HTML. app/plugins/reveal.client.ts
fades in elements with .reveal (add .reveal-delay-1 to -3 to stagger) and turns in the hero's .spin-in compass
rose; everything above a revealed element is revealed too, and content rendered later is picked up. Don't put
.reveal on an element that also has a :class binding: Vue would drop the is-visible class the plugin adds. The
404 needle wanders with a CSS animation. All motion is switched off when the visitor prefers reduced motion.
Header, mobile menu and back to top#
The header gains a border and shadow after scrolling. Below 1024 px the menu button (#menu-button) toggles
#mobile-menu, updates aria-expanded and its label, and closes on Escape (returning focus to the button), when a
link is chosen or when the window grows past 1024 px. BackToTop.vue appears after about one screen of scrolling,
steps aside while the footer's own "Back to top" link is visible and moves focus to the main content. To remove it,
delete <BackToTop /> from app/layouts/default.vue and the .back-to-top styles in main.css.
Forms#
There are four forms. None of them sends anything until you connect it, even though the success states read as if
it had been. The contact form and the booking confirmation show a short reference (BL- and four characters) made
in the browser by makeRef() in app/utils/makeRef.ts; if your form service or booking tool returns its own
reference, show that instead. Check that the promises in those states (a reply within one business day, Nadia
confirming each booking request personally, a confirmation email for the guide) match how you work.
| Form | File | Validates |
|---|---|---|
| Guide sign-up | app/components/GuideForm.vue (home page and resources) |
First name required, email format |
| Contact | app/components/ContactForm.vue |
Name, email, optional phone (7+ digits if filled), topic, message of 20+ characters |
| Booking details | app/components/BookingFlow.vue |
Name, email, role, message of 20+ characters, consent checkbox |
| Quiz | app/components/SelfCheckQuiz.vue |
An answer is chosen before moving on |
Validation is the useValidation() composable in app/composables/useValidation.ts, driven by data-validate
attributes:
data-validate |
Rule |
|---|---|
required |
Not empty (selects: an option chosen) |
email |
Required and shaped like name@company.example |
phone-optional |
Empty, or at least 7 digits |
message |
At least 20 characters |
consent |
A required checkbox that must be checked |
The wording is in fieldMessage(). Errors appear under the field (FieldError.vue), are linked with
aria-describedby, set aria-invalid="true" and clear as the visitor fixes them; the first invalid field receives
focus.
Connecting to Formspree#
Each form's submit handler (onSubmit() in ContactForm.vue and GuideForm.vue) continues only when
v.validate(form) passes; that is the place to send it. Create a form at Formspree, then in ContactForm.vue:
async function onSubmit() {
if (!form.value || !v.validate(form.value)) return;
const res = await fetch("https://formspree.io/f/your-id", {
method: "POST",
body: new FormData(form.value),
headers: { Accept: "application/json" },
});
if (!res.ok) return alert("Sorry, something went wrong. Please email hello@bearing.example.");
// ...then show the thank-you panel as before
}For the guide sign-up you can post to your email tool (Kit, Mailchimp, Buttondown) the same way in GuideForm.vue.
If you deploy with pnpm build (a Node.js server) instead of pnpm generate, you can post to a Nuxt server route
instead (server/api/contact.post.ts with export default defineEventHandler(async (event) => { ... })).
Booking flow#
You have three options:
- Replace it with an embed. In
app/pages/book.vue, replace<BookingFlow />with the inline embed code from Cal.com or Calendly (in a small component wrapped in<ClientOnly>). Keep the page header. - Keep the flow and send a request. In
onDetails()inBookingFlow.vue, post the details plus the chosen time (type.value,s.at.toISOString()) to a form service beforegoTo(4), then confirm each request by hand. Change the step 4 notice to say a confirmation will follow. - Use a scheduling API. Load real free slots from the Cal.com or Calendly API on your own server and replace
slotsFor()inapp/utils/booking.tswith them; never put API keys in client code.
Saved data (localStorage)#
| Key | Shape | Notes |
|---|---|---|
compass-quiz-v1 |
{ "answers": [0, 2, 1, 0, 3, 2], "date": "2026-10-12" } |
One answer index per question |
readQuiz() in app/utils/quiz.ts ignores the value unless answers is an array with exactly one whole-number index
per question, each in range. The result type is always recalculated from the answers, so editing QUIZ_RESULTS never
leaves a stale result. Retake the self-check removes the key. To clear it manually, run
localStorage.removeItem("compass-quiz-v1") in the browser console. Storage goes through safeStorage
(app/utils/safeStorage.ts): if it is blocked, the quiz still works for the page view; it just isn't remembered. If
you change the questions, bump QUIZ_KEY in app/data/quiz.ts (for example to compass-quiz-v2). The saved result is
read after the page loads, so the generated HTML shows the quiz intro.
SEO and sharing#
Each page calls usePageSeo({ title, description }) (app/composables/usePageSeo.ts), which sets the exact
<title>, the description, a canonical link, the Open Graph tags (og:image is public/img/og-image.png) and a
Twitter card. Social networks need absolute URLs, so set your domain when building; it becomes
runtimeConfig.public.siteUrl in nuxt.config.ts:
SITE_URL=https://www.your-domain.com pnpm generateThe 404 page passes noindex: true. Consider adding a sitemap.xml and robots.txt to public/ listing your
routes, and ProfessionalService or Person structured data (JSON-LD, with useHead()) on the home and about pages
with your name, address and services.
Accessibility#
Built in: a skip link, semantic landmarks, one <h1> per page and a logical heading order, visible focus rings
(:focus-visible in main.css), WCAG AA contrast for text, aria-current in the navigation, labeled form fields
with inline errors, aria-pressed filter chips with a live result count, accessible tabs and accordion, a
keyboard-operable calendar, a carousel that never moves on its own, alt text or aria-hidden on every image and
SVG, and reduced-motion support everywhere.
When editing: keep one h1 per page, give every new input a <label>, keep the aria-controls ids of tabs and
accordion buttons in step with their panels, keep alt text meaningful when you swap illustrations for photos, and
check new color pairs for contrast (small text on ivory should be stone-600 or darker).
Building and deploying#
pnpm generate writes a fully static site to .output/public/: one .html file per page (programs.html, served at
/programs), 404.html and the assets in _nuxt/. Deploy it anywhere:
- Netlify / Cloudflare Pages / Vercel: build command
pnpm generate, publish directory.output/public. - Any static host: upload the contents of
.output/public/. Configure the host to serveprograms.htmlfor/programs(most do) and404.htmlfor unknown URLs.
To serve the site from a sub-folder, build with a base path: DEMO_BASE_PATH=/coaching pnpm generate.
nuxt.config.ts passes it to app.baseURL; links get it automatically and public files through asset().
pnpm build:demo does this for the freeframe.dev demo and writes demo/, which you can delete.
Troubleshooting#
| Problem | Fix |
|---|---|
| No time slots on any day | Check AVAILABILITY.hours uses weekday numbers 0 to 6 and "HH:MM" strings, the call length fits inside each window, and daysAhead is larger than minNoticeHours / 24 |
| Times look an hour off around daylight saving changes | Make sure AVAILABILITY.timeZone is an IANA name such as America/New_York, not a fixed offset |
| The quiz always shows an old result | It is saved in localStorage; click Retake the self-check or remove compass-quiz-v1, and bump QUIZ_KEY when you change the questions |
| A filter chip shows 0 or the wrong items | The chip's value must appear exactly (lowercase, no spaces) in the stories' tags or the resources' topic |
| A filtered link shows everything | ?type= and ?topic= must be one of the filter values; unknown values are ignored on purpose |
| SVG text renders in the wrong font | Quote font names that contain digits in font-family attributes, for example 'Source Serif 4' |
| Images or styles are missing under a sub-folder | Build with DEMO_BASE_PATH set to that folder and use asset() for files in public/ |
pnpm lint fails with a missing .nuxt/eslint.config.mjs |
Run pnpm install (or pnpm typecheck) once; nuxt prepare writes it |
License#
Freeframe Commercial License: use it in unlimited websites and apps for yourself and your clients; it may not
be resold or redistributed as a template. The full terms, with your licensee details, are in LICENSE.txt and
at https://freeframe.dev/license#commercial. Third-party components keep their own licenses. Instrument Sans
and Source Serif 4 are included under the SIL Open Font License (app/assets/fonts/OFL.txt). Bearing and all
people, clients, companies and publications named in the demo are fictional.