first commit

This commit is contained in:
2026-09-30 16:19:00 -06:00
commit 43c1b29b02
177 changed files with 40567 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
{{/*
Layer fit — which of the four platform layers a customer path uses.
Input: dict with "fit" (map of layer key -> "yes" | "own" | "no")
and optional "compact" (bool). State is always spelled out in text,
so color is never the only signal.
*/}}
{{ $fit := .fit }}
<ul class="fit{{ if .compact }} fit-compact{{ end }}">
{{ range hugo.Data.layers }}
{{ $state := index $fit .key | default "no" }}
<li class="fit-row fit-{{ .key }} is-{{ $state }}">
<span class="fit-bar" aria-hidden="true"><span></span></span>
<span class="fit-name">{{ .name }}</span>
<span class="fit-state">{{ if eq $state "yes" }}Included{{ else if eq $state "own" }}Your own{{ else }}Add anytime{{ end }}</span>
</li>
{{ end }}
</ul>
+31
View File
@@ -0,0 +1,31 @@
<footer class="foot" data-reveal>
<div class="foot-brand">
<a href="{{ "/" | relURL }}" class="foot-mark" aria-label="Federated Computer, home">
<img src="{{ "images/logo.png" | relURL }}" alt="Federated Computer" class="brand-logo" width="775" height="129">
</a>
<p class="foot-line">The open-source operating system for your business — and the private AI that runs on it.</p>
<span class="foot-place">Colorado Springs, Colorado</span>
<ul class="foot-social">
{{ range site.Menus.footer_social }}<li><a href="{{ .URL }}" target="_blank" rel="noopener">{{ .Name }}<span class="arrow" aria-hidden="true">↗</span></a></li>{{ end }}
</ul>
</div>
<div class="foot-grid">
{{ range (slice (dict "title" "Solutions" "menu" "footer_solutions") (dict "title" "Product" "menu" "footer_product") (dict "title" "Company" "menu" "footer_company") (dict "title" "Get Started" "menu" "footer_start")) }}
<nav class="foot-group" aria-label="{{ .title }}">
<span class="foot-head">{{ .title }}</span>
<ul class="foot-links">
{{ range index site.Menus .menu }}
<li><a href="{{ if or (hasPrefix .URL "mailto:") (hasPrefix .URL "http") }}{{ .URL }}{{ else }}{{ .URL | relURL }}{{ end }}">{{ .Name }}</a></li>
{{ end }}
</ul>
</nav>
{{ end }}
</div>
<div class="foot-legal">
<span class="foot-copy">© {{ now.Format "2006" }} Federated Computer, Inc. FedQ'd in Colorado Springs — built on Federated Computer by a team around the world.</span>
{{ range site.Menus.footer_legal }}<a class="foot-policy" href="{{ .URL | relURL }}">{{ .Name }}</a>{{ end }}
<a class="foot-top" href="#top">Back to top<span class="foot-up" aria-hidden="true">↑</span></a>
</div>
</footer>
+9
View File
@@ -0,0 +1,9 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }} · {{ .Site.Title }}{{ end }}</title>
<meta name="description" content="{{ if .Description }}{{ .Description }}{{ else }}{{ .Site.Params.tagline }}{{ end }}">
<meta name="theme-color" content="#ffffff">
<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=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ "css/style.css" | relURL }}">
+98
View File
@@ -0,0 +1,98 @@
{{/*
The rail: logo, the site's pages ("doors"), the two account actions, and
an index of the sections on this page. Sticky beside the column on wide
screens; a one-line bar with a Menu toggle below 1000px.
*/}}
{{ $page := . }}
<aside class="rail">
<div class="rail-bar">
<a href="{{ "/" | relURL }}" class="brand" aria-label="Federated Computer, home">
<img src="{{ "images/logo.png" | relURL }}" alt="Federated Computer" class="brand-logo" width="775" height="129">
</a>
<details class="menu">
<summary><span class="menu-word">Menu</span><span class="menu-glyph" aria-hidden="true"><i></i><i></i></span></summary>
<nav class="menu-panel" aria-label="Site">
{{ range .Site.Menus.main }}
<a href="{{ .URL | relURL }}" class="menu-top">{{ .Name }}</a>
{{ with .Params.dropdown }}
<div class="menu-sub">
{{ range index $page.Site.Menus . }}<a href="{{ .URL | relURL }}">{{ .Name }}</a>{{ end }}
</div>
{{ end }}
{{ end }}
<div class="menu-actions">
{{ partial "sections/ctas.html" (slice (dict "label" "Start Now" "url" "/start-now/" "style" "primary") (dict "label" "Sign In" "url" "/sign-in/" "style" "ghost")) }}
</div>
</nav>
</details>
</div>
<nav class="nav nav-doors" aria-label="Site">
{{ range .Site.Menus.main }}
{{ $active := or (eq $page.RelPermalink .URL) (and (ne .URL "/") (hasPrefix $page.RelPermalink .URL)) }}
<a href="{{ .URL | relURL }}"{{ if $active }} aria-current="page"{{ end }}>{{ .Name }}</a>
{{ if $active }}
{{ with .Params.dropdown }}
<div class="nav-sub">
{{ range index $page.Site.Menus . }}
<a href="{{ .URL | relURL }}"{{ if eq $page.RelPermalink .URL }} aria-current="page"{{ end }}>{{ .Name }}</a>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ end }}
</nav>
<div class="rail-actions">
<a href="{{ "/start-now/" | relURL }}" class="btn btn-primary btn-sm"><span class="btn-label">Start Now</span></a>
<a href="{{ "/sign-in/" | relURL }}" class="rail-signin"{{ if eq $page.RelPermalink "/sign-in/" }} aria-current="page"{{ end }}>Sign In</a>
</div>
{{ $toc := slice }}
{{ range .Params.sections }}
{{ $label := or .toc .kicker .headline }}
{{ if and $label (not (in (slice "hero" "divider" "final-cta") .type)) }}
{{ $toc = $toc | append (dict "label" ($label | plainify) "id" (partial "section-id.html" .)) }}
{{ end }}
{{ end }}
{{ if $toc }}
<hr class="rail-rule">
<nav class="nav nav-here" aria-label="On this page">
{{ range $toc }}<a href="#{{ .id }}">{{ .label }}</a>{{ end }}
</nav>
{{ end }}
{{/*
"Apps + AI": a slowly scrolling wall of app tiles pinned to the bottom of
the rail (wide screens only). The list is repeated once, hidden from
assistive tech and focus, so the scroll can loop seamlessly. Tiles come
from data/app_wall.yaml; icons from the sprite at images/app-icons.svg.
*/}}
{{ with hugo.Data.app_wall }}
{{ $sprite := "images/app-icons.svg" | relURL }}
<nav class="app-wall" aria-label="Apps + AI">
<a class="app-wall-head" href="{{ "/product/software-library/" | relURL }}">
<span class="app-wall-title">Apps + AI</span>
<span class="app-wall-count">Catalog<span class="arrow" aria-hidden="true">→</span></span>
</a>
<div class="app-wall-view">
<div class="app-wall-scroll">
{{ range $copy := slice 0 1 }}
<ul class="app-wall-grid"{{ if $copy }} aria-hidden="true"{{ end }}>
{{ range hugo.Data.app_wall }}
<li>
<a class="app-tile" href="{{ .url | relURL }}"{{ if $copy }} tabindex="-1"{{ end }}>
{{ with .icon }}<svg class="app-tile-icon" aria-hidden="true"><use href="{{ $sprite }}#{{ . }}"></use></svg>
{{ else }}<span class="app-tile-icon app-tile-letter" aria-hidden="true">{{ substr .name 0 1 }}</span>{{ end }}
<span class="app-tile-name">{{ .name }}</span>
</a>
</li>
{{ end }}
</ul>
{{ end }}
</div>
</div>
</nav>
{{ end }}
</aside>
+11
View File
@@ -0,0 +1,11 @@
{{ range .Params.sections }}
{{ if eq .type "hero" }}
{{ partial "sections/hero.html" . }}
{{ else if eq .type "divider" }}
{{ partial "sections/divider.html" . }}
{{ else if eq .type "final-cta" }}
{{ partial "sections/final-cta.html" . }}
{{ else }}
{{ partial "sections/wrapper.html" . }}
{{ end }}
{{ end }}
+73
View File
@@ -0,0 +1,73 @@
{{/*
The savings calculator: pick the apps you pay for today and your team
size, and compare their yearly retail cost with Federated's annual price.
Input: dict with "key" (unique id), "apps" (name, cost, perSeat, selected),
"annual", optional "note", "compact" (one result, no cost breakdown) and
"more" (a link to the full calculator). The totals need JS; without it,
the app list still shows each app's retail price.
*/}}
{{ $key := .key }}
<form class="savings-calc{{ if .compact }} is-compact{{ end }}" id="{{ $key }}-calc" data-annual="{{ .annual }}" onsubmit="return false">
<fieldset class="calc-apps">
<legend class="plan-subhead">{{ if .compact }}Which of these do you pay for?{{ else }}Which apps do you pay for today?{{ end }}</legend>
<div class="calc-app-grid">
{{ range .apps }}
<label class="calc-app">
<input type="checkbox" value="{{ .cost }}" data-per-seat="{{ .perSeat }}"{{ if .selected }} checked{{ end }}>
<span class="calc-app-name">{{ .name }}</span>
<span class="calc-app-cost">${{ .cost }}{{ if .perSeat }}/person{{ else }} flat{{ end }}</span>
</label>
{{ end }}
</div>
</fieldset>
<div class="calc-users">
<label class="plan-subhead" for="{{ $key }}-users">How many people work at your company?</label>
<div class="calc-users-row">
<input type="range" id="{{ $key }}-users" min="1" max="28" step="1" value="2">
<output class="calc-users-value" for="{{ $key }}-users">10</output>
</div>
</div>
<div class="calc-result" aria-live="polite">
{{ if not .compact }}
<div class="calc-stat"><span class="calc-stat-label">Apps replaced</span><span class="calc-stat-value" data-out="count">0</span></div>
<div class="calc-stat"><span class="calc-stat-label">Their retail cost</span><span class="calc-stat-value" data-out="cost">$0</span><span class="calc-stat-unit">/year</span></div>
{{ end }}
<div class="calc-stat is-highlight"><span class="calc-stat-label">You'd save</span><span class="calc-stat-value" data-out="savings">$0</span><span class="calc-stat-unit">/year</span></div>
{{ with .more }}<a class="more calc-more" href="{{ .url | relURL }}">{{ .label }}<span class="arrow" aria-hidden="true">→</span></a>{{ end }}
</div>
{{ with .note }}<p class="plan-note">{{ . | markdownify }}</p>{{ end }}
</form>
<script>
(function () {
var form = document.getElementById('{{ $key }}-calc');
if (!form) return;
var annual = +form.dataset.annual || 0;
var range = form.querySelector('input[type=range]');
var shown = form.querySelector('.calc-users-value');
var fmt = function (n) { return '$' + Math.round(n).toLocaleString('en-US'); };
function out(name, text) { var el = form.querySelector('[data-out=' + name + ']'); if (el) el.textContent = text; }
// The slider's 28 steps: 1, then 10–100 by 10, 200–1,000 by 100, 1,500–5,000 by 500.
function users(step) {
if (step <= 1) return 1;
if (step <= 11) return (step - 1) * 10;
if (step <= 20) return (step - 10) * 100;
return 1000 + (step - 20) * 500;
}
function update() {
var n = users(+range.value), count = 0, cost = 0;
shown.textContent = n.toLocaleString('en-US') + (+range.value === +range.max ? '+' : '');
range.setAttribute('aria-valuetext', shown.textContent + ' people');
form.querySelectorAll('.calc-app input:checked').forEach(function (box) {
count++;
cost += +box.value * (box.dataset.perSeat === 'true' ? n : 1) * 12;
});
out('count', count);
out('cost', fmt(cost));
out('savings', fmt(Math.max(0, cost - annual)));
}
form.addEventListener('input', update);
update();
})();
</script>
+12
View File
@@ -0,0 +1,12 @@
{{/*
Anchor id for a section: its own `id`, else one made from its `toc`,
`kicker`, or `headline`. Shared by the section wrapper and the rail's
"On this page" index so the two always agree.
*/}}
{{ $id := "" }}
{{ with .id }}
{{ $id = . }}
{{ else }}
{{ with (or .toc .kicker .headline) }}{{ $id = . | plainify | anchorize }}{{ end }}
{{ end }}
{{ return $id }}
@@ -0,0 +1,26 @@
{{/*
Browsable app catalog. Each app may link to its Federated page (`page`,
shown as "Learn more") and to the project's own site (`url`).
*/}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="app-directory">
{{ range $s.categories }}
<details class="app-category">
<summary>
<span class="app-category-name">{{ .name }}</span>
<span class="app-count">{{ len .apps }}</span>
</summary>
<ul class="app-list">
{{ range $app := .apps }}
<li class="app-item">
<strong class="app-name">{{ with $app.page }}<a href="{{ . | relURL }}">{{ $app.name }}</a>{{ else }}{{ $app.name }}{{ end }}</strong>
{{ with $app.description }}<span class="app-desc">{{ . }}</span>{{ end }}
{{ with $app.page }}<a class="app-link" href="{{ . | relURL }}">Learn more.</a>{{ end }}
{{ with $app.url }}<a class="app-link" href="{{ . }}" target="_blank" rel="noopener">{{ if $app.page }}Project site ↗{{ else }}Learn more.{{ end }}</a>{{ end }}
</li>
{{ end }}
</ul>
</details>
{{ end }}
</div>
+4
View File
@@ -0,0 +1,4 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
{{ with $s.intro }}<div class="prose mb-2">{{ . | markdownify }}</div>{{ end }}
<div class="callout"><p>{{ $s.text | markdownify }}</p></div>
+5
View File
@@ -0,0 +1,5 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<ul class="check-list">
{{ range $s.items }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
+20
View File
@@ -0,0 +1,20 @@
{{/*
Buttons. `primary` is the solid blue button with a leading arrow tile,
`ghost` the pale blue secondary with corner ticks, `text` a quiet link
with an arrow. Any trailing "→" in a label is dropped — the arrow is drawn.
*/}}
{{ range . }}
{{ $label := replaceRE `\s*[→↗]\s*$` "" .label }}
{{ $external := or (hasPrefix .url "http") (hasPrefix .url "mailto:") }}
{{ $href := cond (or $external (hasPrefix .url "#")) .url (.url | relURL) }}
{{ if eq .style "ghost" }}
<a href="{{ $href }}" class="btn btn-secondary">{{ $label }}<span class="arrow" aria-hidden="true">{{ if $external }}↗{{ else }}→{{ end }}</span></a>
{{ else if eq .style "text" }}
<a href="{{ $href }}" class="more">{{ $label }}<span class="arrow" aria-hidden="true">{{ if $external }}↗{{ else }}→{{ end }}</span></a>
{{ else }}
<a href="{{ $href }}" class="btn btn-primary">
<span class="btn-tile" aria-hidden="true"><svg viewBox="0 0 16 16" width="14" height="14" fill="none"><path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="square"/></svg></span>
<span class="btn-label">{{ $label }}</span>
</a>
{{ end }}
{{ end }}
+11
View File
@@ -0,0 +1,11 @@
{{ $s := . }}
<div class="stack-diagram">
{{ range $i, $l := $s.layers }}
<div class="layer">
<span class="layer-num">{{ printf "%02d" (add $i 1) }}</span>
<h4>{{ $l.title }}</h4>
<p>{{ $l.body }}</p>
</div>
{{ end }}
</div>
{{ with $s.spineLabel }}<p class="mcp-spine">{{ . }}</p>{{ end }}
+1
View File
@@ -0,0 +1 @@
<hr class="rule-dashed" aria-hidden="true">
+11
View File
@@ -0,0 +1,11 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="faq-list">
{{ range $s.items }}
<details class="faq-item">
<summary>{{ .q }}</summary>
<div class="faq-answer"><p>{{ .a | markdownify }}</p></div>
</details>
{{ end }}
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+8
View File
@@ -0,0 +1,8 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
{{ with $s.intro }}<div class="prose mb-2">{{ . | markdownify }}</div>{{ end }}
<ul class="feature-list">
{{ range $s.items }}
<li><strong>{{ .label }}</strong>{{ .body | markdownify }}</li>
{{ end }}
</ul>
+6
View File
@@ -0,0 +1,6 @@
{{ $s := . }}
<aside class="cta" data-reveal>
<h2 class="cta-title">{{ $s.headline | markdownify }}</h2>
{{ with $s.body }}<p class="cta-text">{{ . | markdownify }}</p>{{ end }}
{{ if $s.ctas }}<div class="actions">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
</aside>
+11
View File
@@ -0,0 +1,11 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="founder-block">
<div class="founder-photo">{{ if $s.photoLabel }}{{ $s.photoLabel }}{{ else }}Photo{{ end }}</div>
<div class="founder-body">
<h3>{{ $s.name }}</h3>
<span class="founder-role">{{ $s.role }}</span>
{{ $s.bio | markdownify }}
{{ if $s.contactUrl }}<p class="mt-1"><a class="more" href="{{ $s.contactUrl }}">{{ replaceRE `\s*[→↗]\s*$` "" $s.contactLabel }}<span class="arrow" aria-hidden="true">→</span></a></p>{{ end }}
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
{{/* Shared section heading: optional kicker (a mono eyebrow), headline, and lede. */}}
{{ if or .kicker .headline .lede }}
<div class="section-head">
{{ with .kicker }}<p class="eyebrow">{{ . }}</p>{{ end }}
{{ with .headline }}<h2>{{ . | markdownify }}</h2>{{ end }}
{{ with .lede }}<p class="lede">{{ . | markdownify }}</p>{{ end }}
</div>
{{ end }}
+109
View File
@@ -0,0 +1,109 @@
{{/*
The opening of every page: an optional note (the eyebrow), the statement,
the dek, the actions — then either the layer panel (when `fit:` is set)
or the field, a blue plate with the platform's four layers drawn as bars.
`bars: false` drops the field.
*/}}
{{ $s := . }}
<header class="hero{{ if $s.compact }} hero-compact{{ end }}">
{{ with $s.eyebrow }}<p class="note" data-reveal><span class="note-live" aria-hidden="true"></span><span class="note-text">{{ . }}</span></p>{{ end }}
<h1 class="statement" data-reveal style="--d:80ms">{{ $s.title | markdownify }}</h1>
{{ with $s.subhead }}<p class="dek" data-reveal style="--d:140ms">{{ . | markdownify }}</p>{{ end }}
{{ if $s.ctas }}<div class="actions" data-reveal style="--d:200ms">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
{{ with $s.log }}
{{/*
An agent's activity log, typed out line by line (see the script below).
Lines take `tag` + `text`; `think: true` marks a reasoning step (greyed,
with a pause); `done: true` closes the run. The window keeps a fixed
height and scrolls as lines arrive. `logImage` swaps the blue field for
a photo; `logCaption` credits it.
*/}}
<figure class="hero-field hero-log{{ if $s.logImage }} has-image{{ end }}" data-reveal style="--d:260ms">
{{ with $s.logImage }}<img class="log-bg" src="{{ . | relURL }}" alt="" fetchpriority="high">{{ end }}
<div class="log-window">
<div class="log-bar">
<span class="log-title">{{ $s.logTitle | default "agent · your-company.federated.computer" }}</span>
<span class="log-live"><span class="note-live" aria-hidden="true"></span>Live</span>
</div>
<div class="log-view">
<ol class="log-lines" aria-label="An AI agent at work on a Federated Core">
{{ range . }}
<li class="log-line{{ if .done }} is-done{{ end }}{{ if .think }} is-think{{ end }}">
<span class="log-tag">{{ .tag | default (cond .done "Done" (cond (.think | default false) "Thinking" "")) }}</span>
<span class="log-text">{{ .text }}</span>
</li>
{{ end }}
</ol>
</div>
</div>
{{ with $s.logCaption }}<figcaption class="log-caption">{{ . }}</figcaption>{{ end }}
</figure>
<script>
(function () {
var fig = document.currentScript.previousElementSibling;
if (!fig || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var view = fig.querySelector('.log-view'), list = fig.querySelector('.log-lines');
var lines = [].slice.call(fig.querySelectorAll('.log-line'));
var onScreen = true, i = 0;
function visible() { return onScreen && !document.hidden; }
fig.classList.add('is-animated');
lines.forEach(function (li) {
li.style.setProperty('--n', li.querySelector('.log-text').textContent.trim().length);
});
// Pause while the log is off screen or the tab is hidden.
function schedule(fn, ms) { setTimeout(function () { if (visible()) fn(); else schedule(fn, 400); }, ms); }
function next() {
if (i === lines.length) { schedule(reset, 4200); return; }
var li = lines[i++], n = +li.style.getPropertyValue('--n');
// Finished lines drop their typing animation, so a repaint can't replay it.
lines.forEach(function (l) { if (l.classList.contains('is-current')) l.classList.add('is-typed'); l.classList.remove('is-current'); });
li.classList.add('is-on', 'is-current');
// Keep the newest line in view, like a terminal: slide the list up.
var shift = Math.max(0, li.offsetTop + li.offsetHeight + 18 - view.clientHeight);
list.style.transform = 'translateY(' + -shift + 'px)';
var pause = li.classList.contains('is-think') ? 1300 : li.classList.contains('is-done') ? 300 : 450;
schedule(next, n * 16 + pause);
}
function reset() {
fig.classList.add('is-clearing');
schedule(function () {
lines.forEach(function (l) { l.classList.remove('is-on', 'is-current', 'is-typed'); });
list.style.transition = 'none';
list.style.transform = '';
void list.offsetHeight;
list.style.transition = '';
fig.classList.remove('is-clearing');
i = 0;
schedule(next, 500);
}, 450);
}
if ('IntersectionObserver' in window) {
new IntersectionObserver(function (e) { onScreen = e[0].isIntersecting; }).observe(fig);
}
schedule(next, 700);
})();
</script>
{{ else }}
{{ with $s.fit }}
<aside class="hero-fit" aria-label="Platform layers this path uses" data-reveal style="--d:260ms">
<div class="hero-fit-head">
<p class="hero-fit-title">What this path uses</p>
<a class="more" href="{{ "/solutions/#compare" | relURL }}">Compare all four<span class="arrow" aria-hidden="true">→</span></a>
</div>
{{ partial "fit.html" (dict "fit" .) }}
</aside>
{{ else }}
{{ if ne $s.bars false }}
<figure class="hero-field" aria-hidden="true" data-reveal style="--d:260ms">
{{/* Listed in stack order; drawn bottom-up so Core is the foundation. */}}
<div class="field-stack">
{{ range $i, $l := hugo.Data.layers }}
<span class="field-layer" style="--i:{{ $i }}"><i></i><em>{{ printf "%02d" (add $i 1) }} · {{ $l.name }}</em></span>
{{ end }}
</div>
</figure>
{{ end }}
{{ end }}
{{ end }}
</header>
+18
View File
@@ -0,0 +1,18 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<ol class="entries layer-list">
{{ range $i, $item := $s.items }}
<li>
{{ $key := "" }}{{ range hugo.Data.layers }}{{ if eq .url $item.link }}{{ $key = .key }}{{ end }}{{ end }}
<a href="{{ $item.link | relURL }}" class="entry layer-row"{{ with $key }} data-layer="{{ . }}"{{ end }}>
<span class="entry-n">{{ printf "%03d" (add $i 1) }}</span>
<span class="layer-body">
<span class="entry-title">{{ $item.title }}</span>
<span class="entry-text">{{ $item.body | markdownify }}</span>
</span>
<span class="entry-go">{{ with $item.linktext }}{{ replaceRE `\s*[→↗]\s*$` "" . }}{{ end }}<span class="arrow" aria-hidden="true">→</span></span>
</a>
</li>
{{ end }}
</ol>
{{ if $s.ctas }}<div class="cta-row">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+31
View File
@@ -0,0 +1,31 @@
{{/*
Capability matrix — rows are capabilities, columns are customer paths.
Cell values: "yes" -> Included, "own" -> Your own, "no" -> Add anytime,
or any other string, shown as written.
*/}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="table-scroll">
<table class="compare-table matrix-table">
<thead>
<tr>
<th scope="col">Capability</th>
{{ range $s.columns }}<th scope="col"><a href="{{ .link | relURL }}">{{ .name }}</a></th>{{ end }}
</tr>
</thead>
<tbody>
{{ range $s.rows }}
<tr>
<th scope="row">{{ .label }}{{ with .layer }}<span class="matrix-layer">{{ . }}</span>{{ end }}</th>
{{ range .cells }}
{{ if eq . "yes" }}<td class="m-yes">Included</td>
{{ else if eq . "own" }}<td class="m-own">Your own</td>
{{ else if eq . "no" }}<td class="m-no">Add anytime</td>
{{ else }}<td class="m-note">{{ . }}</td>{{ end }}
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+57
View File
@@ -0,0 +1,57 @@
{{/*
Customer paths — one row per customer type, each with its layer fit.
`strip: true` adds a four-layer strip pinned above the list: hovering or
focusing a path lights the layers it uses there, and in any `layers`
section on the page (rows carry data-layer). Mouse devices only.
*/}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
{{ if $s.strip }}
<div class="layer-strip" aria-hidden="true">
<span class="layer-strip-label"><span class="strip-idle">Point at a path to see the layers it uses</span><span class="strip-on">Layers this path uses</span></span>
<ol class="layer-strip-list">
{{ range hugo.Data.layers }}<li class="strip-layer" data-layer="{{ .key }}"><i></i><em>{{ .name }}</em></li>{{ end }}
</ol>
</div>
{{ end }}
<ol class="entries path-list">
{{ range $i, $p := $s.items }}
<li>
<a href="{{ $p.link | relURL }}" class="path-row" data-fit="{{ $p.fit | jsonify }}">
<div class="path-body">
<span class="path-who"><span class="entry-n">{{ printf "%03d" (add $i 1) }}</span>{{ $p.who }}</span>
<h3>{{ $p.title }}</h3>
<p>{{ $p.body | markdownify }}</p>
<span class="more path-link">{{ replaceRE `\s*[→↗]\s*$` "" ($p.linktext | default "See this path") }}<span class="arrow" aria-hidden="true">→</span></span>
</div>
<div class="path-fit">{{ partial "fit.html" (dict "fit" $p.fit "compact" true) }}</div>
</a>
</li>
{{ end }}
</ol>
{{ if $s.strip }}
<script>
(function () {
var list = document.currentScript.parentNode.querySelector('.path-list');
if (!list || !matchMedia('(hover: hover)').matches) return;
var targets = document.querySelectorAll('.strip-layer[data-layer], .layer-row[data-layer]');
function light(fit) {
targets.forEach(function (el) {
var state = fit ? (fit[el.dataset.layer] || 'no') : '';
el.classList.toggle('is-lit', state === 'yes');
el.classList.toggle('is-own', state === 'own');
el.classList.toggle('is-off', state === 'no');
});
document.querySelectorAll('.layer-strip').forEach(function (el) { el.classList.toggle('is-active', !!fit); });
}
list.querySelectorAll('.path-row').forEach(function (row) {
var fit = JSON.parse(row.dataset.fit || '{}');
row.addEventListener('mouseenter', function () { light(fit); });
row.addEventListener('focus', function () { light(fit); });
});
list.addEventListener('mouseleave', function () { light(null); });
list.addEventListener('focusout', function (e) { if (!list.contains(e.relatedTarget)) light(null); });
})();
</script>
{{ end }}
{{ if $s.ctas }}<div class="cta-row">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+100
View File
@@ -0,0 +1,100 @@
{{/*
Pricing — one or more plan groups (e.g. "You host" / "We host") behind a
switch. The switch is a set of radio inputs styled as tabs, so it works
without JS; up to four groups. Each group can have:
tiers — bordered rows: name, price, unit, note, features
includes — a checklist shared by every tier, plus an optional note
addons — small priced tiles
apps — a compact table of single apps: name, category, body, price
*/}}
{{ $s := . }}
{{ $key := partial "section-id.html" $s | default "plans" }}
{{ partial "sections/head.html" $s }}
<div class="plan-switch">
{{ if gt (len $s.groups) 1 }}
{{ range $i, $g := $s.groups }}
<input type="radio" class="plan-radio" name="{{ $key }}" id="{{ $key }}-{{ $i }}" aria-label="{{ $g.label }}"{{ if eq $i 0 }} checked{{ end }}>
{{ end }}
<div class="plan-tabs" aria-hidden="true">
{{ range $i, $g := $s.groups }}<label for="{{ $key }}-{{ $i }}">{{ $g.label }}</label>{{ end }}
</div>
{{ end }}
<div class="plan-panels">
{{ range $s.groups }}
<div class="plan-panel"{{ with .id }} id="{{ . }}"{{ end }}>
{{ with .lede }}<p class="plan-lede">{{ . | markdownify }}</p>{{ end }}
{{ with .tiers }}
<ol class="tier-list">
{{ range . }}
<li class="tier">
<div class="tier-head">
<h3 class="tier-name">{{ .name }}</h3>
<p class="tier-price"><span class="tier-amount">{{ .price }}</span>{{ with .unit }}<span class="tier-unit">{{ . }}</span>{{ end }}</p>
{{ with .note }}<p class="tier-note">{{ . | markdownify }}</p>{{ end }}
</div>
<div class="tier-body">
{{ with .intro }}<p class="tier-intro">{{ . | markdownify }}</p>{{ end }}
<ul class="tier-features">
{{ range .features }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
</div>
</li>
{{ end }}
</ol>
{{ end }}
{{ with .includes }}
<div class="plan-block">
{{ with .headline }}<h3 class="plan-subhead">{{ . }}</h3>{{ end }}
<ul class="check-list">
{{ range .items }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
{{ with .note }}<p class="plan-note">{{ . | markdownify }}</p>{{ end }}
</div>
{{ end }}
{{ with .addons }}
<div class="plan-block">
{{ with .headline }}<h3 class="plan-subhead">{{ . }}</h3>{{ end }}
{{ with .lede }}<p class="plan-note plan-note-top">{{ . | markdownify }}</p>{{ end }}
<ul class="addon-row">
{{ range .items }}
<li class="addon">
<h4 class="addon-name">{{ .name }}</h4>
<p class="tier-price"><span class="tier-amount">{{ .price }}</span>{{ with .unit }}<span class="tier-unit">{{ . }}</span>{{ end }}</p>
{{ with .body }}<p class="addon-body">{{ . | markdownify }}</p>{{ end }}
</li>
{{ end }}
</ul>
</div>
{{ end }}
{{ with .apps }}
<div class="plan-block">
{{ with .headline }}<h3 class="plan-subhead">{{ . }}</h3>{{ end }}
{{ with .lede }}<p class="plan-note plan-note-top">{{ . | markdownify }}</p>{{ end }}
<div class="table-scroll">
<table class="compare-table app-price-table">
<thead><tr><th scope="col">App</th><th scope="col">What it does</th><th scope="col">Price</th></tr></thead>
<tbody>
{{ range .items }}
<tr>
<th scope="row">{{ .name }}{{ with .category }}<span class="matrix-layer">{{ . }}</span>{{ end }}</th>
<td>{{ .body | markdownify }}</td>
<td class="app-price">{{ .price }}</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
{{ end }}
{{ with .ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" . }}</div>{{ end }}
</div>
{{ end }}
</div>
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+63
View File
@@ -0,0 +1,63 @@
{{/*
One pull quote (`text`, `cite`), or a stack of them (`items`, each with
`text` and `cite`). With `cards: true`, items render as a two-column grid
of short testimonials: `text`, `name`, optional `role` (Markdown, for a
linked company) and `photo`; without a photo, the person's initials show.
`savings` adds a compact savings calculator below: `apps` (names) and
`selected` (names ticked to start) are looked up in the `savings` section
of the page at `from`, so prices live in one place; `headline` and `more`
(label + url of the full calculator) are optional.
*/}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
{{ $items := $s.items | default (slice (dict "text" $s.text "cite" $s.cite)) }}
{{ if $s.cards }}
<ul class="said-cards">
{{ range $items }}
<li class="said-card">
<blockquote>{{ .text | markdownify }}</blockquote>
<div class="said-who">
{{ with .photo }}
<img class="said-photo" src="{{ . | relURL }}" alt="" width="44" height="44" loading="lazy">
{{ else }}
{{ $initials := "" }}{{ range first 2 (split .name " ") }}{{ $initials = printf "%s%s" $initials (substr . 0 1) }}{{ end }}
<span class="said-photo said-initials" aria-hidden="true">{{ $initials }}</span>
{{ end }}
<span class="said-id">
<span class="said-name">{{ .name }}</span>
{{ with .role }}<span class="said-role">{{ . | markdownify }}</span>{{ end }}
</span>
</div>
</li>
{{ end }}
</ul>
{{ else }}
<div class="said-list">
{{ range $items }}
<figure class="said">
<blockquote>{{ .text | markdownify }}</blockquote>
{{ with .cite }}<figcaption>{{ . | markdownify }}</figcaption>{{ end }}
</figure>
{{ end }}
</div>
{{ end }}
{{ with $s.savings }}
{{ $cfg := . }}
{{ with site.GetPage $cfg.from }}
{{ $src := dict }}
{{ range .Params.sections }}{{ if eq .type "savings" }}{{ $src = . }}{{ end }}{{ end }}
{{ $apps := slice }}
{{ range $name := $cfg.apps }}
{{ range $src.apps }}{{ if eq .name $name }}
{{ $apps = $apps | append (dict "name" .name "cost" .cost "perSeat" .perSeat "selected" (in ($cfg.selected | default slice) $name)) }}
{{ end }}{{ end }}
{{ end }}
<div class="said-savings">
{{ with $cfg.headline }}<h3 class="said-savings-title">{{ . }}</h3>{{ end }}
{{ partial "savings-calc.html" (dict "key" (printf "%s-savings" (partial "section-id.html" $s)) "apps" $apps "annual" $src.federatedAnnual "compact" true "more" $cfg.more) }}
</div>
{{ else }}
{{ errorf "quote section: savings.from page %q not found" $cfg.from }}
{{ end }}
{{ end }}
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+31
View File
@@ -0,0 +1,31 @@
{{/*
Savings — an optional bar comparison (`bars`: name, amount, highlight) and
a savings calculator (`apps`: name, cost, perSeat, selected; compared with
`federatedAnnual`), rendered by partials/savings-calc.html.
*/}}
{{ $s := . }}
{{ $key := partial "section-id.html" $s | default "savings" }}
{{ partial "sections/head.html" $s }}
{{ with $s.bars }}
{{ $max := 0 }}
{{ range . }}{{ if gt .amount $max }}{{ $max = .amount }}{{ end }}{{ end }}
<figure class="bar-chart">
{{ with $s.barsTitle }}<figcaption class="bar-chart-title">{{ . }}</figcaption>{{ end }}
<ol class="bar-list">
{{ range . }}
<li class="bar-row{{ if .highlight }} is-highlight{{ end }}">
<span class="bar-name">{{ .name }}</span>
<span class="bar-track"><span class="bar-fill" style="width: {{ div (mul .amount 100.0) $max }}%"></span></span>
<span class="bar-value">${{ lang.FormatNumber 0 .amount }}</span>
</li>
{{ end }}
</ol>
{{ with $s.barsNote }}<p class="plan-note">{{ . | markdownify }}</p>{{ end }}
</figure>
{{ end }}
{{ with $s.apps }}
{{ partial "savings-calc.html" (dict "key" $key "apps" . "annual" $s.federatedAnnual "note" $s.calcNote) }}
{{ end }}
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+14
View File
@@ -0,0 +1,14 @@
{{/* Two columns, each a heading plus a short list. */}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="split">
{{ range $s.columns }}
<div class="split-col">
<h3>{{ .title }}</h3>
<ul class="split-list">
{{ range .items }}<li>{{ . | markdownify }}</li>{{ end }}
</ul>
</div>
{{ end }}
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+45
View File
@@ -0,0 +1,45 @@
{{/*
Stat tiles. Values that start with a number ("80%+", "100+", "30X") count
up from zero when the row scrolls into view; others show as written.
Without JS or with reduced motion, every value simply shows.
*/}}
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="stat-row">
{{ range $s.items }}
<div class="stat">
<span class="stat-value">{{ .value }}</span>
<span class="stat-label">{{ .label }}</span>
</div>
{{ end }}
</div>
<script>
(function () {
var row = document.currentScript.previousElementSibling;
if (!('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var counters = [];
row.querySelectorAll('.stat-value').forEach(function (el) {
var m = el.textContent.trim().match(/^(\d+)(.*)$/);
if (!m) return;
var shown = document.createElement('span'), full = el.textContent.trim();
shown.setAttribute('aria-hidden', 'true');
shown.textContent = '0' + m[2];
el.innerHTML = '<span class="visually-hidden"></span>';
el.firstChild.textContent = full;
el.appendChild(shown);
counters.push({ node: shown, to: +m[1], suffix: m[2] });
});
if (!counters.length) return;
new IntersectionObserver(function (entries, io) {
if (!entries[0].isIntersecting) return;
io.disconnect();
var start = performance.now(), dur = 1200;
(function tick(now) {
var t = Math.min(1, (now - start) / dur), eased = 1 - Math.pow(1 - t, 3);
counters.forEach(function (c) { c.node.textContent = Math.round(c.to * eased) + c.suffix; });
if (t < 1) requestAnimationFrame(tick);
})(start);
}, { threshold: 0.6 }).observe(row);
})();
</script>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+13
View File
@@ -0,0 +1,13 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<ol class="step-row{{ if eq (len $s.items) 4 }} steps-4{{ end }}">
{{ range $i, $item := $s.items }}
<li class="step">
<div class="step-head">
<h3>{{ $item.title }}</h3>
<span class="step-num">{{ printf "%03d" (add $i 1) }}</span>
</div>
<p>{{ $item.body | markdownify }}</p>
</li>
{{ end }}
</ol>
+28
View File
@@ -0,0 +1,28 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="table-scroll">
<table class="compare-table">
<thead>
<tr>
<th></th>
{{ range $s.headers }}<th>{{ . }}</th>{{ end }}
</tr>
</thead>
<tbody>
{{ range $s.rows }}
{{ $n := len .cells }}
<tr>
<th>{{ .label }}</th>
{{ range $i, $cell := .cells }}
{{ if eq (add $i 1) $n }}
<td class="good">{{ $cell }}</td>
{{ else }}
<td class="bad">{{ $cell }}</td>
{{ end }}
{{ end }}
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+6
View File
@@ -0,0 +1,6 @@
{{ $s := . }}
{{ partial "sections/head.html" $s }}
<div class="prose{{ if $s.centered }} centered{{ end }}">
{{ $s.body | markdownify }}
</div>
{{ if $s.ctas }}<div class="cta-row mt-2">{{ partial "sections/ctas.html" $s.ctas }}</div>{{ end }}
+5
View File
@@ -0,0 +1,5 @@
{{ $s := . }}
{{ $id := partial "section-id.html" $s }}
<section class="section section-{{ $s.type }}"{{ with $id }} id="{{ . }}"{{ end }} data-reveal>
{{ partial (printf "sections/%s.html" $s.type) $s }}
</section>