feat: certificates, landing pages, ai tutor, website update, compliance

This commit is contained in:
Rotimi Best
2026-05-18 03:47:22 +01:00
parent c31ad63955
commit ac3a2c14a7
644 changed files with 56050 additions and 5439 deletions
+1 -8
View File
@@ -6,9 +6,6 @@
body {
overflow-x: hidden;
}
* {
font-family: 'Geist', sans-serif;
}
@@ -138,11 +135,7 @@ body {
}
svg.lucide-icon:not(.custom) {
color: #666666;
}
.dark svg.lucide-icon:not(.custom) {
color: #fff;
color: inherit;
}
@media (min-width: 640px) {
+15 -12
View File
@@ -8,8 +8,23 @@
<!-- Fonts as <link> (not @import in app.css) so Tailwind isn’t blocked on first paint -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Geist (site body font) — synchronous so first paint has the brand font. -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" />
<!-- JetBrains Mono (code blocks) — non-blocking. Preload as a stylesheet, then swap rel to apply. -->
<link
rel="preload"
as="style"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap"
/>
</noscript>
<!-- <title>ClassroomIO –&nbsp;The Open Source Learning Management System for Companies</title>
<meta name="description" content="ClassroomIO" />
<meta name="application-name" content="ClassroomIO" />
@@ -54,18 +69,6 @@
data-website-id="5321d227-751a-4d43-8556-f516fba60891"
></script>
<script defer type="text/javascript">
window.$crisp = [];
window.CRISP_WEBSITE_ID = '69176b47-5f63-42cc-bb9b-68d4927b6276';
(function () {
d = document;
s = d.createElement('script');
s.src = 'https://client.crisp.chat/l.js';
s.async = 1;
d.getElementsByTagName('head')[0].appendChild(s);
})();
</script>
<script defer type="text/javascript">
(function (C, A, L) {
let p = function (a, ar) {
@@ -0,0 +1,86 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import LayoutTemplate from '@lucide/svelte/icons/layout-template';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import Terminal from '@lucide/svelte/icons/terminal';
const lanes = [
{
Icon: LayoutTemplate,
title: 'Pick a template',
description: 'Product 101, onboarding, or expert track. Fill three fields, get a course draft.'
},
{
Icon: MessagesSquare,
title: 'Chat with the builder',
description: 'Claude lives in the editor. It plans, drafts, and writes quizzes — you approve each step.'
},
{
Icon: Terminal,
title: 'Use your own agent',
description: 'Drive course authoring from Cursor, Claude Code, Codex, or OpenCode via MCP.'
}
];
const agents = [
{ name: 'Cursor', logo: '/cursor-logo.png' },
{ name: 'Claude Code', logo: '/claude-logo.svg' },
{ name: 'Codex', logo: '/codex-logo.png' },
{ name: 'OpenCode', logo: '/opencode-logo.svg' }
];
</script>
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<BlurFade once>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">AI authoring</div>
<h2 class="max-w-[680px] text-[clamp(1.75rem,3vw,2.6rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Build entire courses with AI.
</h2>
<p class="mt-3 max-w-[560px] text-sm leading-relaxed text-gray-500">
Generate the structure, lessons, and quizzes from your docs — then review and approve every step before it
ships. Three ways in. Pick whichever fits your team.
</p>
</BlurFade>
<BlurFade delay={0.1} once>
<div class="mx-auto mt-10 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/ai-course-builder.jpeg"
alt="Course builder — plan, approve, implement. Templates, in-editor chat, and MCP."
class="block h-auto w-full"
loading="lazy"
decoding="async"
/>
</div>
</div>
</BlurFade>
<div class="mt-10 grid grid-cols-1 gap-8 md:grid-cols-3">
{#each lanes as lane, i}
<BlurFade delay={0.05 * i + 0.15} once>
<div>
<div class="text-gray-950">
<lane.Icon size={20} strokeWidth={1.6} />
</div>
<h3 class="mt-3 text-sm font-medium text-gray-950 lg:text-base">{lane.title}</h3>
<p class="mt-1.5 max-w-[280px] text-xs leading-relaxed text-gray-500 lg:text-sm">{lane.description}</p>
</div>
</BlurFade>
{/each}
</div>
<BlurFade delay={0.3} once>
<div class="mt-10 flex flex-wrap items-center gap-x-6 gap-y-3 border-t border-gray-200 pt-6">
<span class="text-[10px] font-medium tracking-widest text-gray-500 uppercase">Works with</span>
{#each agents as agent}
<div class="flex items-center gap-1.5">
<img src={agent.logo} alt="{agent.name} logo" class="h-4 w-4 rounded-full" />
<span class="text-xs text-gray-600">{agent.name}</span>
</div>
{/each}
</div>
</BlurFade>
</div>
</section>
@@ -1,6 +1,23 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
import CodeBlock from './code-block.svelte';
// mcp.json — sky-300 keys · emerald-300 strings · slate-500 punctuation
// 2-space indent (real spaces; body uses whitespace-pre).
const mcpJsonLines = [
'<span class="text-slate-500">{</span>',
' <span class="text-sky-300">"mcpServers"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"classroomio"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"command"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"npx"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"args"</span><span class="text-slate-500">: [</span><span class="text-emerald-300">"-y"</span><span class="text-slate-500">,</span> <span class="text-emerald-300">"@classroomio/mcp"</span><span class="text-slate-500">],</span>',
' <span class="text-sky-300">"env"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"CLASSROOMIO_API_URL"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"https://api.classroomio.com"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"CLASSROOMIO_API_KEY"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"&lt;your-mcp-key&gt;"</span>',
' <span class="text-slate-500">}</span>',
' <span class="text-slate-500">}</span>',
' <span class="text-slate-500">}</span>',
'<span class="text-slate-500">}</span>'
];
const aiFeatures = [
{
@@ -72,29 +89,12 @@
<BlurFade delay={0.2} once>
<div>
<!-- Code Block -->
<div class="relative overflow-hidden rounded-2xl border border-gray-800 bg-[#0d0d0d]">
<BorderBeam size={200} duration={10} colorFrom="#0233BD" colorTo="#6b8fff" />
<div class="flex items-center gap-2 border-b border-gray-800 bg-gray-900 px-5 py-3.5">
<div class="h-2.5 w-2.5 rounded-full bg-red-400"></div>
<div class="h-2.5 w-2.5 rounded-full bg-yellow-400"></div>
<div class="h-2.5 w-2.5 rounded-full bg-green-500"></div>
<span class="ml-2 text-xs text-gray-600">mcp.json</span>
</div>
<div class="p-6 font-mono text-xs leading-loose whitespace-pre text-gray-400 lg:text-sm">
{'{'}<br />{' '}<span class="text-blue-400">"mcpServers"</span>: {'{'}<br />{' '}<span
class="text-blue-400">"classroomio"</span
>: {'{'}<br />{' '}<span class="text-blue-400">"command"</span>:
<span class="text-green-400">"npx"</span>,<br />{' '}<span class="text-blue-400">"args"</span>:
[<span class="text-green-400">"-y"</span>, <span class="text-green-400">"@classroomio/mcp"</span>],<br
/>{' '}<span class="text-blue-400">"env"</span>: {'{'}<br />{' '}<span
class="text-blue-400">"CLASSROOMIO_API_URL"</span
>: <span class="text-green-400">"https://api.classroomio.com"</span>,<br />{' '}<span
class="text-blue-400">"CLASSROOMIO_API_KEY"</span
>: <span class="text-green-400">"&lt;your-mcp-key&gt;"</span><br />{' }'}<br />{' }'}<br
/>{' }'}<br />{'}'}
</div>
</div>
<CodeBlock
fileName="mcp.json"
lang="JSON"
codeLines={mcpJsonLines}
statusBar="paste into ~/.claude/mcp.json · restart your client"
/>
<!-- Supported Agents -->
<div class="mt-6">
@@ -0,0 +1,166 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import { BotIcon, BotOffIcon } from '@cio/ui/custom/moving-icons';
import ImagePlaceholder from './image-placeholder.svelte';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
/**
* AI teaching assistant section — centered, light theme, logo on top, gradient accent,
* large heading, short subhead, optional CTA, and an image placeholder underneath.
* Used on the homepage + each solution page with variant copy.
*
* @typedef {Object} Props
* @property {string} [accent] Small gradient line above the heading (e.g. "Academy", "Compliance AI")
* @property {string} [title] Main heading
* @property {string} description Subhead paragraph (1–3 sentences)
* @property {string} [ctaLabel] CTA button label (optional). Omit to hide the button.
* @property {string} [ctaHref] CTA destination
* @property {string} [imagePath] Suggested static file path for the image placeholder
* @property {string} [imageCaption] Caption shown inside the placeholder
* @property {boolean} [showImage] Show the image placeholder (default true)
* @property {string} [imageSrc] Real image path. When set, renders a real <img> (takes priority over vimeoVideoId).
* @property {string} [imageAlt] Alt text for the real image
* @property {string} [vimeoVideoId] Vimeo video id. When provided (and no imageSrc), replaces the image placeholder.
* @property {string} [videoAspect] CSS padding-top percentage controlling the video aspect ratio (default "54.68%")
* @property {string} [videoTitle] Accessible title for the iframe
* @property {string} [bgClass] Section background (default bg-white)
*/
/** @type {Props} */
let {
accent = 'Academy',
title = 'AI-powered Teaching Assistant',
description,
ctaLabel,
ctaHref = '/customer-education',
imagePath = '/static/ai-tutor-stack.png',
imageCaption = 'AI tutor in a lesson — lesson-aware chat, follow-ups, translation, summarisation',
showImage = true,
imageSrc,
imageAlt = '',
vimeoVideoId,
videoAspect = '54.68%',
videoTitle = 'AI Teaching Assistant on ClassroomIO.',
bgClass = 'bg-white'
} = $props();
/** @type {HTMLElement | null} */
let sectionEl = $state(null);
let inZone = $state(true);
const inZoneBorder = 'border-blue-500';
const outZoneBorder = 'border-yellow-500';
const inZoneBot = '#2563eb';
const outZoneBot = '#eab308';
</script>
<section
bind:this={sectionEl}
role="region"
aria-label={title}
onmouseenter={() => (inZone = true)}
onmouseleave={() => (inZone = false)}
class="{bgClass} {inZone
? inZoneBorder
: outZoneBorder} border-t-2 border-dashed px-6 py-12 transition-colors duration-200 lg:px-12 lg:py-16"
>
<div class="mx-auto max-w-[1100px] text-center">
<BlurFade once>
<div class="relative mx-auto mb-2 h-12 w-12">
<div
class="absolute inset-0 flex items-center justify-center transition-opacity duration-300 ease-in-out"
style="opacity: {inZone ? 1 : 0}; pointer-events: {inZone ? 'auto' : 'none'};"
aria-hidden={!inZone}
>
<BotIcon
size={48}
strokeWidth={1.6}
color={inZoneBot}
trackElement={sectionEl}
trackRadius={420}
maxEyeOffset={2.2}
/>
</div>
<div
class="absolute inset-0 flex items-center justify-center transition-opacity duration-300 ease-in-out"
style="opacity: {inZone ? 0 : 1}; pointer-events: {inZone ? 'none' : 'auto'};"
aria-hidden={inZone}
>
<BotOffIcon size={48} strokeWidth={1.6} color={outZoneBot} />
</div>
</div>
<p class="mb-6 text-[11px] text-gray-400">(you have full control over what i say and do)</p>
</BlurFade>
{#if accent}
<BlurFade delay={0.05} once>
<p
class="bg-gradient-to-r from-blue-600 via-fuchsia-500 to-orange-400 bg-clip-text text-[clamp(1.4rem,2.2vw,2rem)] leading-[1.1] font-medium tracking-tight text-transparent"
>
{accent}
</p>
</BlurFade>
{/if}
<BlurFade delay={0.1} once>
<h2
class="mx-auto max-w-[820px] text-[clamp(1.75rem,3.4vw,2.6rem)] leading-[1.1] font-medium tracking-tight text-gray-950"
>
{title}
</h2>
</BlurFade>
<BlurFade delay={0.2} once>
<p class="mx-auto mt-4 max-w-[600px] text-sm leading-relaxed text-gray-500">
{description}
</p>
</BlurFade>
{#if ctaLabel}
<BlurFade delay={0.3} once>
<div class="mt-6 flex justify-center">
<a
href={ctaHref}
class="inline-flex items-center gap-2 rounded-full bg-gray-950 px-5 py-2.5 text-sm font-medium text-white no-underline transition-all hover:-translate-y-0.5 hover:bg-blue-700"
>
{ctaLabel}
<ArrowRight size={14} />
</a>
</div>
</BlurFade>
{/if}
{#if !imageSrc && !vimeoVideoId && showImage}
<BlurFade delay={0.35} once>
<div class="mx-auto mt-10 w-full max-w-[820px]">
<ImagePlaceholder suggestedFile={imagePath} caption={imageCaption} aspect="aspect-[16/9]" />
</div>
</BlurFade>
{/if}
</div>
{#if imageSrc}
<BlurFade delay={0.35} once>
<div class="mx-auto mt-10 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img src={imageSrc} alt={imageAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
</div>
</div>
</BlurFade>
{:else if vimeoVideoId}
<BlurFade delay={0.35} once>
<div class="mx-auto mt-10 w-full max-w-[960px] overflow-hidden rounded-lg">
<div style="padding:{videoAspect} 0 0 0;position:relative;">
<iframe
src="https://player.vimeo.com/video/{vimeoVideoId}?badge=0&autopause=0&player_id=0&app_id=58479&autoplay=1&muted=1&loop=1"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
referrerpolicy="strict-origin-when-cross-origin"
style="position:absolute;top:0;left:0;width:100%;height:100%;"
title={videoTitle}
></iframe>
</div>
</div>
</BlurFade>
{/if}
</section>
@@ -7,16 +7,14 @@
'Full API access to every object in your LMS. Create users, enroll learners, pull completions. All programmatically, at scale.'
},
{
icon: '⚡',
title: 'Zapier Integration',
comingSoon: true,
icon: '🤖',
title: 'MCP server',
description:
'Connect to 6,000+ apps without writing a line of code. Auto-enroll from your CRM, send Slack alerts on completion, update your spreadsheet.'
'Drive course authoring from Cursor, Claude Code, Codex, or OpenCode. The MCP server exposes the API surface as agent tools — list, create, update, publish.'
},
{
icon: '🪝',
title: 'Webhooks',
comingSoon: true,
description:
'Real-time event notifications for every action. Course enrolled, lesson completed, certificate issued. Push to any endpoint instantly.'
}
@@ -40,15 +38,7 @@
class="rounded-2xl border border-gray-200 bg-white p-8 transition-all duration-200 hover:border-gray-950 hover:shadow-lg hover:shadow-black/5"
>
<div class="mb-4 text-3xl">{item.icon}</div>
<h3 class="mb-2 text-base font-medium">
{item.title}
{#if item.comingSoon}
<span
class="ml-1 rounded-full bg-blue-700 px-1.5 py-0.5 align-middle text-[0.6rem] tracking-wide text-white uppercase"
>Soon</span
>
{/if}
</h3>
<h3 class="mb-2 text-base font-medium">{item.title}</h3>
<p class="text-sm leading-relaxed text-gray-500">{item.description}</p>
</div>
{/each}
@@ -0,0 +1,55 @@
<script>
import BoxedGrid from './boxed-grid.svelte';
import Webhook from '@lucide/svelte/icons/webhook';
import Code2 from '@lucide/svelte/icons/code-2';
import Sparkles from '@lucide/svelte/icons/sparkles';
const lanes = [
{
Icon: Code2,
title: 'REST API',
description: 'Every object, reachable over HTTP. Provision users, pull completions, build your own tools.'
},
{
Icon: Webhook,
title: 'Webhooks',
description: 'Signed events on enrollment, completion, certificate. Retries on failure.'
},
{
Icon: Sparkles,
title: 'MCP server',
description: 'Course authoring as agent tools. Works with Cursor, Claude Code, Codex, OpenCode.'
}
];
</script>
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-8 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">For developers</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">API, Webhooks, MCP.</h2>
<p class="mt-3 text-sm leading-relaxed text-gray-500">
No middleware in between. Wire it into the stack you already have.
</p>
</div>
<BoxedGrid items={lanes} cols={3} padding="p-5 lg:p-6">
{#snippet cell(lane)}
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-xl bg-blue-50 text-blue-700">
<lane.Icon size={16} strokeWidth={1.75} />
</div>
<h3 class="text-sm font-medium text-gray-950 lg:text-base">{lane.title}</h3>
<p class="mt-1.5 text-xs leading-relaxed text-gray-500 lg:text-sm">{lane.description}</p>
{/snippet}
</BoxedGrid>
<div class="mt-6">
<a
href="/automation"
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-950 no-underline transition-all hover:border-gray-950"
>
See the developer reference →
</a>
</div>
</div>
</section>
@@ -0,0 +1,53 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
/**
* Boxed grid of cards — one outer rounded border, internal dividers between cells.
* Reused by RoiBand, AutomationBand, customer-ed outcomes, etc.
*
* @typedef {Object} Props
* @property {any[]} items
* @property {import('svelte').Snippet<[any, number]>} cell Snippet that renders one cell's content
* @property {2 | 3 | 4} [cols] Desktop column count (default 3)
* @property {string} [bgClass] Background class for the container (default bg-white)
* @property {string} [padding] Cell padding (default "p-6 lg:p-8")
* @property {boolean} [animate] Wrap each cell in BlurFade (default true)
*/
/** @type {Props} */
let { items, cell, cols = 3, bgClass = 'bg-white', padding = 'p-6 lg:p-8', animate = true } = $props();
const colsClass = {
2: 'sm:grid-cols-2',
3: 'sm:grid-cols-3',
4: 'sm:grid-cols-4'
}[cols];
</script>
<div class="grid grid-cols-1 overflow-hidden rounded-2xl border border-gray-200 {bgClass} {colsClass}">
{#each items as item, i (i)}
{#if animate}
<BlurFade delay={0.05 * i} once>
<div
class="h-full border-gray-200 {padding}"
class:border-t={i > 0}
class:sm:border-l={i % cols !== 0}
class:sm:border-t-0={i < cols}
class:sm:border-t={i >= cols}
>
{@render cell(item, i)}
</div>
</BlurFade>
{:else}
<div
class="h-full border-gray-200 {padding}"
class:border-t={i > 0}
class:sm:border-l={i % cols !== 0}
class:sm:border-t-0={i < cols}
class:sm:border-t={i >= cols}
>
{@render cell(item, i)}
</div>
{/if}
{/each}
</div>
@@ -0,0 +1,30 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import ImagePlaceholder from './image-placeholder.svelte';
</script>
<section class="bg-white px-6 py-16 lg:px-12 lg:py-24">
<div class="mx-auto max-w-[1100px]">
<BlurFade delay={0} once>
<div class="mb-10 max-w-[620px]">
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">Certificate templates</div>
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
Design certificates that
<em class="text-blue-700 not-italic">look like yours.</em>
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
A real design canvas — drag elements, swap brand colours and fonts, configure the ID format. Issue PDFs and
PNGs learners actually want to share.
</p>
</div>
</BlurFade>
<BlurFade delay={0.1} once>
<ImagePlaceholder
suggestedFile="/static/certificate-editor.png"
caption="Screenshot of the certificate template editor"
aspect="aspect-[16/9]"
/>
</BlurFade>
</div>
</section>
@@ -0,0 +1,69 @@
<script>
import { onMount } from 'svelte';
import { renderCertificateDocument, DEFAULT_CERTIFICATE_DESIGN } from '@cio/certificates';
/**
* Renders the default ClassroomIO certificate template (classique) in an iframe.
* Used on customer-education and compliance-training pages.
*
* @typedef {Object} Props
* @property {string} recipient Person's name
* @property {string} achievement Course or program name
* @property {string} issued Issue date (e.g. "11 May 2026")
* @property {string} verifyValue Certificate ID rendered in the seal
* @property {string} [orgName] Org name shown on the top tag (default "ClassroomIO")
* @property {string} [courseDescription] Body description (default derived from achievement)
*/
/** @type {Props} */
let {
recipient,
achievement,
issued,
verifyValue,
orgName = 'ClassroomIO',
courseDescription = `For successfully completing ${achievement}.`
} = $props();
const srcdoc = renderCertificateDocument(DEFAULT_CERTIFICATE_DESIGN, {
recipientName: recipient,
courseName: achievement,
courseDescription,
orgName,
date: issued,
certificateId: verifyValue
});
const NATIVE_W = 1100;
const NATIVE_H = 780;
/** @type {HTMLDivElement | null} */
let stageEl = $state(null);
let scale = $state(0.5);
onMount(() => {
if (!stageEl || typeof ResizeObserver === 'undefined') return;
const observer = new ResizeObserver(() => {
if (!stageEl) return;
const rect = stageEl.getBoundingClientRect();
if (rect.width > 0) scale = rect.width / NATIVE_W;
});
observer.observe(stageEl);
return () => observer.disconnect();
});
</script>
<div
bind:this={stageEl}
class="relative overflow-hidden rounded-2xl border border-gray-200 shadow-2xl/10"
style="aspect-ratio: {NATIVE_W} / {NATIVE_H};"
>
<iframe
title="Certificate preview"
sandbox="allow-same-origin"
{srcdoc}
class="absolute top-0 left-0 border-0"
style="width: {NATIVE_W}px; height: {NATIVE_H}px; transform: scale({scale}); transform-origin: top left;"
></iframe>
</div>
@@ -0,0 +1,89 @@
<script>
/**
* @typedef {Object} Props
* @property {string} fileName Filename shown in chrome (e.g. "mcp.json")
* @property {string[]} codeLines HTML-formatted source lines (one entry per visual line)
* @property {string} [lang] Optional language badge (e.g. "JSON", "HTML", "TypeScript")
* @property {string} [statusBar] Optional status bar text
* @property {boolean} [copyable] Show a Copy button (default true)
* @property {boolean} [showLineNumbers] Show line-number gutter (default false)
* @property {string} [class] Extra wrapper classes
*/
/** @type {Props} */
let {
fileName,
codeLines,
lang = '',
statusBar = '',
copyable = true,
showLineNumbers = false,
class: className = ''
} = $props();
</script>
<div
class="relative overflow-hidden rounded-2xl bg-[#0d1117] shadow-[0_20px_60px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/[0.08] {className}"
>
<!-- Editor chrome -->
<div class="flex items-center justify-between border-b border-white/[0.06] bg-[#0a0e14] px-4 py-3">
<div class="flex items-center gap-2">
<div class="h-2.5 w-2.5 rounded-full bg-[#ff5f57]"></div>
<div class="h-2.5 w-2.5 rounded-full bg-[#febc2e]"></div>
<div class="h-2.5 w-2.5 rounded-full bg-[#28c840]"></div>
<span class="cio-code-font ml-3 text-[12px] text-slate-500">{fileName}</span>
</div>
<div class="flex items-center gap-2">
{#if lang}
<span
class="cio-code-font rounded-md bg-white/[0.04] px-2 py-0.5 text-[10px] font-medium tracking-wider text-slate-400 uppercase"
>{lang}</span
>
{/if}
{#if copyable}
<span
class="cio-code-font cursor-pointer rounded-md bg-blue-500/10 px-2 py-0.5 text-[10px] font-medium tracking-wide text-blue-300 ring-1 ring-blue-500/20 transition-colors hover:bg-blue-500/15"
>Copy</span
>
{/if}
</div>
</div>
<!-- Body -->
<div class="cio-code-font flex text-[13px] leading-[1.6] text-slate-300">
{#if showLineNumbers}
<div class="py-5 pr-3 pl-5 text-right text-slate-600 tabular-nums select-none">
{#each codeLines as _, n}
<div>{n + 1}</div>
{/each}
</div>
{/if}
<div class="flex-1 overflow-x-auto whitespace-pre {showLineNumbers ? 'pr-5' : 'px-5'} py-5">
{#each codeLines as line}
<div>{@html line || ' '}</div>
{/each}
</div>
</div>
{#if statusBar}
<div class="cio-code-font border-t border-white/[0.06] bg-black/40 px-4 py-2 text-[10.5px] text-slate-500">
<span class="text-blue-300">●</span>
{statusBar}
</div>
{/if}
</div>
<style>
/* JetBrains Mono loaded via the Google Fonts link in src/app.html. System stack as fallback. */
/* :global(...*) forces the font onto every descendant — including {@html}-rendered
spans that don't carry Svelte's scope hash, and overrides any stray universal rules. */
.cio-code-font,
:global(.cio-code-font *) {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-feature-settings:
'liga' 0,
'calt' 0;
font-variant-ligatures: none;
letter-spacing: 0;
}
</style>
@@ -0,0 +1,102 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import Code2 from '@lucide/svelte/icons/code-2';
import CodeBlock from './code-block.svelte';
/**
* @typedef {Object} Capability
* @property {string} label
* @property {string} sub
*/
/**
* @typedef {Object} Props
* @property {string} eyebrow Eyebrow label (e.g. "Embed anywhere")
* @property {string} title Section heading
* @property {string} description Body paragraph
* @property {string} fileName Filename shown in code-editor chrome (e.g. "partner-portal.html")
* @property {string[]} codeLines HTML-formatted lines for the code body (each may contain tag spans)
* @property {string} statusBar Bottom status line text
* @property {string} previewUrl URL shown in the preview browser bar
* @property {string} previewImageSrc Static path for the preview image
* @property {string} [previewAlt] Alt text for the preview image
* @property {Capability[]} [capabilities] Optional 3 capability bullets shown below the pair
* @property {string} [bgClass] Section background (default bg-white)
*/
/** @type {Props} */
let {
eyebrow,
title,
description,
fileName,
codeLines,
statusBar,
previewUrl,
previewImageSrc,
previewAlt = '',
capabilities = [],
bgClass = 'bg-white'
} = $props();
</script>
<section class="px-6 py-16 lg:px-12 lg:py-24 {bgClass}">
<div class="mx-auto max-w-[1100px]">
<!-- Intro -->
<div class="mb-12 grid grid-cols-1 gap-10 lg:grid-cols-[0.85fr_1.15fr] lg:gap-20">
<BlurFade delay={0} once>
<div>
<div class="mb-3 inline-flex items-center gap-2 text-xs font-medium tracking-widest text-blue-700 uppercase">
<Code2 size={14} />
{eyebrow}
</div>
<h2 class="text-[clamp(2rem,3vw,2.6rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
</div>
</BlurFade>
<BlurFade delay={0.1} once>
<p class="text-base leading-relaxed text-gray-500">{description}</p>
</BlurFade>
</div>
<!-- Code → Preview pair -->
<div class="grid grid-cols-1 items-center gap-6 lg:grid-cols-[0.95fr_1.05fr]">
<!-- LEFT: Code editor -->
<BlurFade delay={0.05} once>
<CodeBlock {fileName} {codeLines} lang="HTML" {statusBar} />
</BlurFade>
<!-- RIGHT: Preview -->
<BlurFade delay={0.15} once>
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl/5">
<!-- Browser chrome -->
<div class="flex items-center gap-2 border-b border-gray-200 bg-white px-4 py-3">
<div class="h-2 w-2 rounded-full bg-red-400"></div>
<div class="h-2 w-2 rounded-full bg-yellow-400"></div>
<div class="h-2 w-2 rounded-full bg-green-500"></div>
<div class="ml-3 flex flex-1 items-center gap-2 rounded-md bg-gray-50 px-3 py-1.5 text-xs text-gray-500">
<span class="text-green-600">●</span>
{previewUrl}
</div>
<span class="rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-medium text-blue-700">Renders as →</span>
</div>
<img src={previewImageSrc} alt={previewAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
</div>
</BlurFade>
</div>
<!-- Capabilities -->
{#if capabilities.length}
<div class="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{#each capabilities as item, i}
<BlurFade delay={0.05 * i} once>
<div class="rounded-xl border border-gray-200 bg-white px-4 py-3">
<p class="text-sm font-medium text-gray-950">{item.label}</p>
<p class="text-xs text-gray-500">{item.sub}</p>
</div>
</BlurFade>
{/each}
</div>
{/if}
</div>
</section>
@@ -1,6 +1,5 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
import { DotPattern } from '@cio/ui/custom/animation/dot-pattern';
const features = [
@@ -146,8 +145,6 @@
<div class="relative">
<div class="relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.03] p-6">
<BorderBeam colorFrom="#3b82f6" colorTo="#0233BD" size={200} duration={8} />
<div class="mb-5 flex items-center gap-2">
<div class="h-3 w-3 rounded-full bg-white/[0.15]"></div>
<div class="h-3 w-3 rounded-full bg-white/[0.1]"></div>
@@ -0,0 +1,98 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import ArrowRight from '@lucide/svelte/icons/arrow-right';
/**
* @typedef {Object} Feature
* @property {any} Icon
* @property {string} title
* @property {string} description
*/
/**
* @typedef {Object} Props
* @property {string} eyebrow
* @property {string} title
* @property {string} [description]
* @property {string} [ctaLabel]
* @property {string} [ctaHref]
* @property {Feature[]} features
* @property {2|3|4} [columns] Desktop columns (default 3)
* @property {string} [bgClass]
*/
/** @type {Props & { headerAside?: import('svelte').Snippet }} */
let {
eyebrow,
title,
description,
ctaLabel,
ctaHref,
features,
columns = 3,
bgClass = 'bg-white',
headerAside
} = $props();
// Per-columns class string so Tailwind picks up the arbitrary nth-child variants statically.
const gridClass = {
2: 'sm:grid-cols-2 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0',
3: 'sm:grid-cols-2 lg:grid-cols-3 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-child(2n)]:lg:border-r [&>*:nth-child(3n)]:lg:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0 [&>*:nth-last-child(-n+3)]:lg:border-b-0',
4: 'sm:grid-cols-2 lg:grid-cols-4 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-child(2n)]:lg:border-r [&>*:nth-child(4n)]:lg:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0 [&>*:nth-last-child(-n+4)]:lg:border-b-0'
}[columns];
</script>
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 grid grid-cols-1 gap-10 lg:grid-cols-[1.05fr_1fr] lg:gap-20">
<div>
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(2rem,3.2vw,2.75rem)] leading-[1.05] font-medium tracking-tight text-gray-950">
{title}
</h2>
</div>
{#if headerAside}
<div class="flex flex-col justify-start lg:pt-2">
{@render headerAside()}
</div>
{:else if description || ctaLabel}
<div class="flex flex-col justify-start lg:pt-2">
{#if description}
<p class="text-base leading-relaxed text-gray-600">{description}</p>
{/if}
{#if ctaLabel && ctaHref}
<a
href={ctaHref}
class="mt-6 inline-flex w-fit items-center gap-2 text-sm font-medium text-blue-700 underline decoration-blue-300 underline-offset-4 transition-colors hover:text-blue-800 hover:decoration-blue-500"
>
{ctaLabel}
<ArrowRight size={16} />
</a>
{/if}
</div>
{/if}
</div>
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white">
<div class="grid grid-cols-1 {gridClass}">
{#each features as feature, i}
<div class="group relative border-b border-gray-200 transition-colors hover:bg-gray-50/60 sm:border-r">
<BlurFade delay={0.04 * i} once>
<div class="h-full p-6">
<p class="font-mono text-[11px] tracking-[0.15em] text-gray-400">
{String(i + 1).padStart(2, '0')}
</p>
<div class="mt-4 flex items-center gap-2.5">
<span class="text-blue-700"><feature.Icon size={18} /></span>
<h3 class="text-sm font-semibold text-gray-950">{feature.title}</h3>
</div>
<p class="mt-3 text-sm leading-relaxed text-gray-500">{feature.description}</p>
</div>
</BlurFade>
</div>
{/each}
</div>
</div>
</div>
</section>
@@ -1,84 +1,207 @@
<script>
<script lang="ts">
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import Palette from '@lucide/svelte/icons/palette';
import Globe from '@lucide/svelte/icons/globe';
import Code2 from '@lucide/svelte/icons/code-2';
import Settings2 from '@lucide/svelte/icons/settings-2';
import Lock from '@lucide/svelte/icons/lock';
import { onDestroy } from 'svelte';
const features = [
{
Icon: Palette,
iconBg: 'bg-rose-50',
iconColor: 'text-rose-600',
title: 'Full Theming',
description: 'Pick the colors, fonts, and logo. Your brand shows up everywhere your learners look.',
tags: ['Custom colors', 'Logo upload', 'Font control']
},
{
Icon: Globe,
iconBg: 'bg-blue-50',
iconColor: 'text-blue-600',
title: 'Custom Domain',
description:
'Host the LMS on learn.yourcompany.com with SSL included. No ClassroomIO branding anywhere on the page.',
tags: ['SSL included', 'White label', 'DNS setup guide']
},
{
Icon: Code2,
iconBg: 'bg-violet-50',
iconColor: 'text-violet-600',
title: 'Embed courses on your site',
description:
'Add one script tag to any page. Your courses render right there and send learners back to your LMS to enroll.',
tags: ['One-line script', 'Auto-styled', 'Links to your domain']
},
{
Icon: Settings2,
iconBg: 'bg-emerald-50',
iconColor: 'text-emerald-600',
title: 'Deep LMS Customization',
description:
'The codebase is modern and readable. Your developers can change anything without fighting a plugin system.',
tags: ['Modern stack', 'Clean codebase', 'Extensible']
}
const ROTATION_INTERVAL_MS = 3000;
let selectedTheme = $state('blue');
let intervalId = $state<NodeJS.Timeout | null>(null);
const themes = [
{ id: 'blue', color: '#1d4ee2', bgClass: 'bg-blue-700/20', activeBorder: 'border-[#1d4ee2]' },
{ id: 'rose', color: '#be1241', bgClass: 'bg-red-600/20', activeBorder: 'border-[#be1241]' },
{ id: 'green', color: '#0c891b', bgClass: 'bg-green-600/20', activeBorder: 'border-[#0c891b]' },
{ id: 'orange', color: '#cc4902', bgClass: 'bg-orange-600/20', activeBorder: 'border-[#cc4902]' },
{ id: 'purple', color: '#9333ea', bgClass: 'bg-purple-600/20', activeBorder: 'border-purple-600' }
];
function startRotation() {
if (intervalId) return;
intervalId = setInterval(() => {
const currentIndex = themes.findIndex((t) => t.id === selectedTheme);
const nextIndex = (currentIndex + 1) % themes.length;
selectedTheme = themes[nextIndex].id;
}, ROTATION_INTERVAL_MS);
}
function stopRotation() {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
function selectTheme(themeId: string) {
selectedTheme = themeId;
stopRotation();
}
startRotation();
onDestroy(() => {
stopRotation();
});
</script>
<section class="bg-gray-50 px-6 py-16 lg:px-12 lg:py-24">
<section class="bg-[#f5f4f0] px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<BlurFade once>
<div class="mb-3.5 text-xs font-medium tracking-widest text-blue-700 uppercase">Make it yours</div>
<h2 class="mb-4 text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
Your brand.<br />Your domain. Your rules.
<h2
class="max-w-[820px] text-[clamp(1.75rem,2.8vw,2.4rem)] leading-[1.1] font-medium tracking-tight text-gray-950"
>
Make it yours,<br />down to the favicon.
</h2>
<p class="mb-14 max-w-[520px] text-base leading-relaxed text-gray-500 lg:text-lg">
ClassroomIO bends to fit your organization, not the other way around.
<p class="mt-3 max-w-[620px] text-sm leading-relaxed text-gray-500">
Your brand, your domain, your content. ClassroomIO bends to fit the academy you want to ship.
</p>
</BlurFade>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
{#each features as feature, i}
<BlurFade delay={0.1 * i} once>
<div class="rounded-2xl border border-gray-200 bg-white p-8 lg:p-9">
<div
class="mb-5 flex h-12 w-12 items-center justify-center rounded-xl {feature.iconBg} {feature.iconColor}"
>
<feature.Icon class="h-6 w-6" strokeWidth={1.75} />
</div>
<h3 class="mb-2.5 text-xl font-medium tracking-tight">{feature.title}</h3>
<p class="mb-5 text-sm leading-relaxed text-gray-500">{feature.description}</p>
<div class="flex flex-wrap gap-2">
{#each feature.tags as tag}
<span
class="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-1 text-xs font-medium text-gray-500"
<div class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-2">
<!-- Theme + branding -->
<BlurFade delay={0.05} once>
<div
class="flex h-full flex-col rounded-2xl border border-gray-200/80 p-6 transition-colors duration-500 lg:p-7 {themes.find(
(t) => t.id === selectedTheme
)?.bgClass}"
>
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Theme everything</h3>
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
Pick a theme color or drop in your own hex. Every page picks it up.
</p>
<!-- Visual: actual theme swatches from the app -->
<div class="mt-8 flex flex-1 flex-col justify-end">
<div class="flex flex-wrap items-center gap-2.5">
{#each themes as theme (theme.id)}
<button
type="button"
class="flex size-8 cursor-pointer items-center justify-center rounded-full border-2 bg-white transition-all {selectedTheme ===
theme.id
? theme.activeBorder
: 'border-transparent'}"
aria-label="{theme.id} theme"
onclick={() => selectTheme(theme.id)}
>
{tag}
</span>
<span class="size-5 rounded-full" style="background-color: {theme.color}"></span>
</button>
{/each}
<div
class="flex h-9 w-9 items-center justify-center rounded-full border-2 border-dashed border-gray-400 text-gray-500"
aria-label="Custom hex"
>
+
</div>
</div>
</div>
</BlurFade>
{/each}
</div>
</BlurFade>
<!-- Custom domain -->
<BlurFade delay={0.1} once>
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Your domain</h3>
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
Map a CNAME, click verify. SSL is handled. No ClassroomIO branding on the page.
</p>
<!-- Visual: browser bar mock -->
<div class="mt-8 flex flex-1 flex-col justify-end">
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm">
<div class="flex items-center gap-2 border-b border-gray-100 bg-white px-3 py-2.5">
<div class="h-2 w-2 rounded-full bg-red-400"></div>
<div class="h-2 w-2 rounded-full bg-yellow-400"></div>
<div class="h-2 w-2 rounded-full bg-green-500"></div>
<div
class="ml-2 flex flex-1 items-center gap-1.5 rounded-md bg-gray-50 px-2.5 py-1 text-[11px] text-gray-600"
>
<Lock size={10} class="text-green-600" />
<span class="font-mono">learn.acme.com</span>
</div>
</div>
<div class="grid grid-cols-3 gap-1.5 p-3">
<div class="h-12 rounded-md bg-gradient-to-br from-blue-200 to-blue-400"></div>
<div class="h-12 rounded-md bg-gradient-to-br from-indigo-200 to-blue-500"></div>
<div class="h-12 rounded-md bg-gradient-to-br from-sky-200 to-cyan-400"></div>
</div>
</div>
</div>
</div>
</BlurFade>
<!-- Embed -->
<BlurFade delay={0.15} once>
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Embed anywhere</h3>
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
One script tag. Drop courses into your help center, in-product, or marketing site.
</p>
<!-- Visual: code snippet -->
<div class="mt-8 flex flex-1 flex-col justify-end">
<div
class="overflow-hidden rounded-xl border border-gray-800/90 bg-gray-950 p-4 font-mono text-[11px] leading-relaxed text-gray-300 shadow-sm"
>
<p class="text-gray-500">&lt;!-- in your help center --&gt;</p>
<p class="mt-1.5">
<span class="text-pink-400">&lt;script</span>
<span class="text-sky-300"> src</span>=<span class="text-emerald-300">"cdn.cio/widget.js"</span><span
class="text-pink-400">&gt;&lt;/script&gt;</span
>
</p>
<p class="mt-1.5">
<span class="text-pink-400">&lt;div</span>
<span class="text-sky-300"> data-cio-widget</span>=<span class="text-emerald-300">"onboarding"</span
><span class="text-pink-400">&gt;&lt;/div&gt;</span>
</p>
</div>
</div>
</div>
</BlurFade>
<!-- Open source / deep customization -->
<BlurFade delay={0.2} once>
<div class="flex h-full flex-col rounded-2xl border border-gray-200/80 bg-[#ecebe6] p-6 lg:p-7">
<h3 class="text-base font-medium tracking-tight text-gray-950 lg:text-lg">Fork the codebase</h3>
<p class="mt-2 max-w-[420px] text-sm leading-relaxed text-gray-500">
Modern stack, readable codebase, AGPL on GitHub. Change whatever you need.
</p>
<!-- Visual: stylised file tree / stack -->
<div class="mt-8 flex flex-1 flex-col justify-end">
<div class="space-y-1.5">
<div
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
>
<span class="h-1.5 w-1.5 rounded-full bg-emerald-500"></span>
<span class="font-mono">apps/dashboard</span>
<span class="ml-auto text-gray-400">Svelte 5</span>
</div>
<div
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
>
<span class="h-1.5 w-1.5 rounded-full bg-blue-500"></span>
<span class="font-mono">apps/api</span>
<span class="ml-auto text-gray-400">Hono · Drizzle</span>
</div>
<div
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
>
<span class="h-1.5 w-1.5 rounded-full bg-fuchsia-500"></span>
<span class="font-mono">packages/ai-assistant</span>
<span class="ml-auto text-gray-400">Claude</span>
</div>
<div
class="flex items-center gap-2 rounded-md bg-white/70 px-3 py-2 text-[11px] font-medium text-gray-700 ring-1 ring-gray-200"
>
<span class="h-1.5 w-1.5 rounded-full bg-amber-500"></span>
<span class="font-mono">packages/mcp</span>
<span class="ml-auto text-gray-400">MCP</span>
</div>
</div>
</div>
</div>
</BlurFade>
</div>
</div>
</section>
+170 -88
View File
@@ -1,128 +1,210 @@
<script>
import { Button } from '@cio/ui/base/button';
import Facebook from '@lucide/svelte/icons/facebook';
import Youtube from '@lucide/svelte/icons/youtube';
import Twitter from '@lucide/svelte/icons/twitter';
</script>
<div class="border-t px-[2%] lg:px-[14%]">
<div class="flex flex-col items-center justify-between py-[10%] pl-5 lg:flex-row lg:pl-0">
<div class="w-full min-w-[200px] lg:w-3/12">
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="classroomio logo" class="mb-5" />
<p class="mr-2 mb-5 text-sm leading-7 tracking-wide text-gray-600 lg:text-base lg:leading-7">
The Open Source Learning Management System for Companies
<br />
The LMS for compliance training and certification programs.
<footer
class="ui:border-border relative overflow-hidden border-t bg-[linear-gradient(to_top,#eef3fb_0%,#f8fafc_100%)] px-[5%] py-12 lg:px-[10%]"
>
<!-- Minor cloud — top-left, smaller than the hero -->
<svg
class="pointer-events-none absolute -top-4 left-3 z-[2] h-12 w-[160px] md:-top-5 md:left-4 md:h-16 md:w-[210px] lg:-top-6 lg:h-20 lg:w-[260px]"
viewBox="0 0 240 110"
fill="none"
aria-hidden="true"
>
<path
d="M 22 80
C 22 60, 42 50, 62 56
C 68 36, 100 30, 116 52
C 130 32, 168 38, 174 60
C 196 56, 216 68, 214 82
C 214 96, 196 102, 174 96
C 164 106, 130 106, 120 96
C 110 106, 78 106, 66 94
C 46 100, 22 96, 22 80 Z"
fill="white"
stroke="#94a3b8"
stroke-width="1.5"
/>
</svg>
<!-- Second smaller cloud, offset right -->
<svg
class="pointer-events-none absolute top-3 left-32 z-[2] h-9 w-[110px] md:top-4 md:left-44 md:h-12 md:w-[140px] lg:top-6 lg:left-[200px] lg:h-14 lg:w-[180px]"
viewBox="0 0 200 90"
fill="none"
aria-hidden="true"
>
<path
d="M 16 64
C 16 48, 34 40, 52 46
C 58 30, 86 26, 100 44
C 114 28, 144 34, 150 52
C 168 48, 184 58, 182 70
C 182 82, 168 86, 150 82
C 142 90, 114 90, 106 82
C 98 90, 70 90, 60 80
C 42 86, 16 82, 16 64 Z"
fill="white"
stroke="#94a3b8"
stroke-width="1.5"
/>
</svg>
<div class="relative z-10 flex flex-col gap-10 lg:flex-row lg:justify-between">
<!-- Brand -->
<div class="flex flex-col gap-4 lg:max-w-[220px]">
<img width="32" height="32" loading="lazy" src="/logo-192.png" alt="ClassroomIO logo" />
<p class="ui:text-muted-foreground text-sm leading-6">
The Open Source LMS for compliance training and certification programs.
</p>
<!-- <a
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
target="_blank"
><img
loading="lazy"
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=425236&theme=light"
alt="ClassroomIO - The&#0032;Open&#0032;Source&#0032;Platform&#0032;for&#0032;Tech&#0032;Bootcamps | Product Hunt"
style="width: 250px; height: 54px;"
width="250"
height="54"
/></a
> -->
<div class="mt-5 flex items-center gap-3 lg:mt-10 lg:gap-7">
<Button
<div class="flex items-center gap-2">
<a
href="https://www.facebook.com/classroomiohq"
target="_blank"
variant="ghost"
aria-label="Facebook Link"
aria-label="Facebook"
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
>
<Facebook />
</Button>
<Button href="https://twitter.com/classroomio" target="_blank" variant="ghost" aria-label="Twitter Link">
<img width="20" height="20" loading="lazy" src="/twitter.svg" alt="" />
</Button>
<Button href="https://www.youtube.com/@ClassroomIO" target="_blank" variant="ghost" aria-label="Youtube Link">
<Youtube />
</Button>
<Facebook size={18} />
</a>
<a
href="https://twitter.com/classroomio"
target="_blank"
aria-label="Twitter"
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
>
<Twitter size={18} />
</a>
<a
href="https://www.youtube.com/@ClassroomIO"
target="_blank"
aria-label="YouTube"
class="ui:text-muted-foreground ui:hover:text-foreground p-1 transition-colors"
>
<Youtube size={18} />
</a>
</div>
</div>
<nav
class="mt-16 flex w-full flex-col items-start justify-end gap-12 space-y-6 lg:mt-0 lg:w-3/4 lg:flex-row lg:space-y-0"
>
<div class="max-w-[120px]">
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Product</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#features"> Certifications </a>
<!-- Nav columns -->
<nav class="grid grid-cols-2 gap-8 sm:grid-cols-4">
<div>
<p class="ui:text-foreground mb-4 text-sm font-semibold">Product</p>
<ul class="space-y-3">
<li>
<a href="/mcp-recipes" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>MCP Recipes</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#ai"> AI Content </a>
<li>
<a href="/automation" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>API & MCP</a
>
</li>
<!-- <li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#deploy"> Deploy </a>
</li> -->
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/#testimonial"> Testimonials </a>
<li>
<a href="/pricing" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Pricing</a
>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Solutions</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/employee-training">Employee Training</a>
<div>
<p class="ui:text-foreground mb-4 text-sm font-semibold">Solutions</p>
<ul class="space-y-3">
<li>
<a
href="/customer-education"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Customer Education</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/bootcamps">Bootcamps</a>
<li>
<a
href="/compliance-training"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Compliance Training</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/customer-education">Customer Education</a>
<li>
<a
href="/partner-training"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Partner Training</a
>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Resources</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/contact">Contact us</a>
<div>
<p class="ui:text-foreground mb-4 text-sm font-semibold">Resources</p>
<ul class="space-y-3">
<li>
<a
href="/docs-vs-company-academy"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Docs vs Company Academy</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/teach">Teach with us</a>
<li>
<a href="/teach" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Teach with us</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
<li>
<a
href="https://classroomio.com/docs/"
target="_blank"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Help Center</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
<li>
<a
href="https://classroomio.com/docs/quickstart/signup"
target="_blank"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Getting Started</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/oss-friends"> OSS Friends </a>
<li>
<a href="/oss-friends" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>OSS Friends</a
>
</li>
</ul>
</div>
<div class="max-w-[120px]">
<p class="mb-3 text-base leading-6 font-semibold text-gray-900 lg:mb-10 lg:text-base">Company</p>
<ul class="space-y-2 lg:space-y-4">
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/roadmap"> Roadmap </a>
<div>
<p class="ui:text-foreground mb-4 text-sm font-semibold">Company</p>
<ul class="space-y-3">
<li>
<a href="/roadmap" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Roadmap</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="mailto:help@classroomio.com" target="_blank"> Support </a>
<li>
<a
href="mailto:help@classroomio.com"
target="_blank"
class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors">Support</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/privacy"> Privacy policy </a>
<li>
<a href="/privacy" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Privacy Policy</a
>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup"> Getting Started </a>
</li>
<li class="text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900 lg:text-base">
<a href="/tos"> Terms of use </a>
<li>
<a href="/tos" class="ui:text-muted-foreground ui:hover:text-foreground text-sm transition-colors"
>Terms of Use</a
>
</li>
</ul>
</div>
</nav>
</div>
<footer class="border-t py-3 text-center text-xs leading-5 font-light text-gray-500 lg:text-left lg:text-base">
<p class="mx-auto w-fit">All rights reserved - ClassroomIO © {new Date().getFullYear()}</p>
</footer>
</div>
<div
class="ui:border-border ui:text-muted-foreground relative z-10 mt-10 border-t pt-6 text-center text-xs lg:text-left"
>
All rights reserved — ClassroomIO © {new Date().getFullYear()}
</div>
</footer>
@@ -0,0 +1,18 @@
<script>
/**
* Standard hero subtitle paragraph used across marketing pages.
* Defaults: `mt-6 max-w-[640px] text-lg leading-relaxed text-gray-600`.
* Pass `class` to override or extend (e.g. a different max-width).
*
* @typedef {Object} Props
* @property {string} [class]
* @property {import('svelte').Snippet} [children]
*/
/** @type {Props} */
let { class: className = '', children } = $props();
</script>
<p class="mt-6 max-w-[640px] text-lg leading-relaxed text-gray-600 {className}">
{@render children?.()}
</p>
+161 -124
View File
@@ -1,139 +1,176 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import ParticleField from './particle-field.svelte';
</script>
<section class="px-6 pt-32 pb-16 lg:px-12 lg:pt-40 lg:pb-24">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div>
<BlurFade delay={0} once>
<a href="/blog/learn-from-the-best" class="mb-7 inline-block">
<Badge variant="secondary" class="gap-2! px-3.5! py-1.5! text-xs! underline!">
<span class="relative flex h-2 w-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
</span>
Introducing ClassroomIO Version 2 →
</Badge>
</a>
</BlurFade>
<section class="relative overflow-hidden px-6 pt-32 pb-16 lg:px-12 lg:pt-40 lg:pb-24">
<!-- Soft off-white base with the faintest blue undertone -->
<div
class="pointer-events-none absolute inset-0 bg-[linear-gradient(135deg,#f7f9fc_0%,#eef2fa_60%,#e5ecf7_100%)]"
></div>
<BlurFade delay={0.1} once>
<h1 class="mb-6 text-[clamp(2.4rem,4vw,3.4rem)] leading-[1.12] font-medium tracking-tight text-gray-950">
The LMS for
<em class="text-blue-700 not-italic">compliance training</em>
and
<em class="text-blue-700 not-italic">certification programs.</em>
</h1>
</BlurFade>
<!-- Bordered clouds, top-left — crisp SVG silhouettes -->
<div class="pointer-events-none absolute inset-0 z-[1] overflow-hidden">
<!-- Cloud 1 (largest) -->
<svg
class="cio-cloud-1 absolute top-6 left-3 h-24 w-[220px] md:top-8 md:left-5 md:h-36 md:w-[340px] lg:top-10 lg:left-6 lg:h-48 lg:w-[460px]"
viewBox="0 0 240 110"
fill="none"
aria-hidden="true"
>
<path
d="M 22 80
C 22 60, 42 50, 62 56
C 68 36, 100 30, 116 52
C 130 32, 168 38, 174 60
C 196 56, 216 68, 214 82
C 214 96, 196 102, 174 96
C 164 106, 130 106, 120 96
C 110 106, 78 106, 66 94
C 46 100, 22 96, 22 80 Z"
fill="white"
stroke="#cbd5e1"
stroke-width="0.75"
/>
</svg>
<BlurFade delay={0.2} once>
<p class="mb-9 max-w-[480px] text-base leading-relaxed text-gray-500 lg:text-lg">
ClassroomIO is the open source LMS built for compliance-driven teams. Run your certification programs, track
completions, and prove training happened. No Moodle headaches.
</p>
</BlurFade>
<!-- Cloud 2 (medium) -->
<svg
class="cio-cloud-2 absolute top-24 left-40 h-16 w-[150px] md:top-32 md:left-56 md:h-24 md:w-[230px] lg:top-40 lg:left-72 lg:h-36 lg:w-[320px]"
viewBox="0 0 200 90"
fill="none"
aria-hidden="true"
>
<path
d="M 16 64
C 16 48, 34 40, 52 46
C 58 30, 86 26, 100 44
C 114 28, 144 34, 150 52
C 168 48, 184 58, 182 70
C 182 82, 168 86, 150 82
C 142 90, 114 90, 106 82
C 98 90, 70 90, 60 80
C 42 86, 16 82, 16 64 Z"
fill="white"
stroke="#cbd5e1"
stroke-width="0.75"
/>
</svg>
<BlurFade delay={0.3} once>
<div class="flex flex-wrap items-center gap-3.5">
<a
href="/signup"
class="inline-flex items-center gap-2 rounded-lg bg-gray-950 px-7 py-3.5 text-sm font-medium text-white no-underline transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-700 lg:text-base"
>
Get Started Free →
</a>
<button
data-cal-link="classroomio/demo"
data-cal-config="'layout':'month_view'"
class="cursor-pointer rounded-lg border-[1.5px] border-gray-200 bg-transparent px-7 py-3.5 text-sm font-medium text-gray-950 transition-all duration-200 hover:border-gray-950 lg:text-base"
>
Book a Demo
</button>
</div>
</BlurFade>
<!-- Cloud 3 (small, lower) -->
<svg
class="cio-cloud-3 absolute top-40 left-16 h-14 w-[120px] md:top-56 md:left-20 md:h-20 md:w-[180px] lg:top-72 lg:left-28 lg:h-28 lg:w-[240px]"
viewBox="0 0 180 80"
fill="none"
aria-hidden="true"
>
<path
d="M 14 56
C 14 42, 30 36, 44 40
C 50 26, 76 22, 88 38
C 100 24, 128 30, 134 46
C 150 42, 166 52, 164 62
C 164 72, 150 76, 134 72
C 128 80, 100 80, 92 72
C 84 80, 60 80, 52 70
C 36 76, 14 72, 14 56 Z"
fill="white"
stroke="#cbd5e1"
stroke-width="0.75"
/>
</svg>
</div>
<BlurFade delay={0.4} once>
<div class="mt-7 flex flex-wrap items-center gap-5 text-xs text-gray-500">
<span class="flex items-center gap-1.5">
<span class="font-medium text-blue-700">✓</span> Open source
</span>
<span class="flex items-center gap-1.5">
<span class="font-medium text-blue-700">✓</span> Self-hosted
</span>
<span class="flex items-center gap-1.5">
<span class="font-medium text-blue-700">✓</span> No per-seat fees
</span>
</div>
</BlurFade>
</div>
<!-- Particle cloud — same component used in the footer at lower intensity -->
<ParticleField class="absolute inset-0 z-[2] h-full w-full" />
<!-- Bottom fade to next section -->
<div
class="pointer-events-none absolute right-0 bottom-0 left-0 h-32 bg-gradient-to-b from-transparent to-white"
></div>
<div class="relative z-10 mx-auto flex max-w-[1100px] flex-col items-center text-center">
<BlurFade delay={0} once>
<h1 class="max-w-[820px] text-[clamp(2rem,4vw,3.25rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Teach your customers your product
<em class="text-blue-700 not-italic">before support has to.</em>
</h1>
</BlurFade>
<BlurFade delay={0.1} once>
<p class="mt-5 max-w-[580px] text-sm leading-relaxed lg:text-base">
Launch a branded customer academy in days. Train customers, certify power users, and drive adoption on your own
domain, with an AI tutor in every lesson.
</p>
</BlurFade>
<!-- Hero Visual -->
<BlurFade delay={0.2} once>
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 shadow-2xl/5">
<BorderBeam size={250} duration={12} colorFrom="#0233BD" colorTo="#6b8fff" delay={1} />
<div class="flex items-center gap-2 border-b border-gray-200 bg-white px-4 py-3.5">
<div class="h-2.5 w-2.5 rounded-full bg-red-400"></div>
<div class="h-2.5 w-2.5 rounded-full bg-yellow-400"></div>
<div class="h-2.5 w-2.5 rounded-full bg-green-500"></div>
</div>
<div class="p-6">
<div class="mb-3.5 text-xs font-medium tracking-wider text-gray-500 uppercase">Team Progress - This Week</div>
<div class="mt-7 flex flex-wrap items-center justify-center gap-3">
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
<Button href="/signup" variant="outline">Start free</Button>
</div>
</BlurFade>
</div>
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-blue-50 text-lg">📋</div>
<div class="min-w-0 flex-1">
<div class="mb-1 text-sm font-medium">Annual Compliance Training</div>
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
<div class="h-full rounded-full bg-blue-700" style="width: 85%"></div>
</div>
</div>
<div
class="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-blue-700"
>
85%
</div>
</div>
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-green-50 text-lg">🛡️</div>
<div class="min-w-0 flex-1">
<div class="mb-1 text-sm font-medium">Security & Compliance</div>
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
<div class="h-full rounded-full bg-green-500" style="width: 100%"></div>
</div>
</div>
<div
class="shrink-0 rounded-full bg-green-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-green-600"
>
✓ Done
</div>
</div>
<div class="mb-3 flex items-center gap-3.5 rounded-xl border border-gray-200 bg-white p-4">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-orange-50 text-lg">🎯</div>
<div class="min-w-0 flex-1">
<div class="mb-1 text-sm font-medium">Product Certification</div>
<div class="h-1 overflow-hidden rounded-full bg-gray-200">
<div class="h-full rounded-full bg-blue-700" style="width: 42%"></div>
</div>
</div>
<div
class="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[0.7rem] font-medium whitespace-nowrap text-blue-700"
>
42%
</div>
</div>
<div class="mt-4 flex items-center gap-3 rounded-lg bg-blue-50 p-4">
<span class="text-xl">🏆</span>
<div>
<div class="text-sm font-medium">3 certificates issued today</div>
<div class="text-xs text-gray-500">Sarah, Marcus & Priya completed their certs</div>
</div>
</div>
</div>
<div class="relative z-30 mx-auto mt-14 w-full max-w-[1100px]">
<BlurFade delay={0.3} once>
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/hero-academy.jpeg"
alt="Branded customer academy on a custom domain — catalog, programs, AI tutor"
width="2560"
height="1440"
class="block h-auto w-full"
loading="eager"
decoding="async"
/>
</div>
</BlurFade>
</div>
</section>
<style>
.cio-cloud-1 {
animation: cio-cloud-drift-1 22s ease-in-out infinite;
}
.cio-cloud-2 {
animation: cio-cloud-drift-2 28s ease-in-out infinite;
}
.cio-cloud-3 {
animation: cio-cloud-drift-3 26s ease-in-out infinite;
}
@keyframes cio-cloud-drift-1 {
0%,
100% {
transform: translate(0, 0) scale(1);
opacity: 0.9;
}
50% {
transform: translate(28px, 10px) scale(1.04);
opacity: 1;
}
}
@keyframes cio-cloud-drift-2 {
0%,
100% {
transform: translate(0, 0) scale(1);
opacity: 0.85;
}
50% {
transform: translate(36px, -8px) scale(1.06);
opacity: 1;
}
}
@keyframes cio-cloud-drift-3 {
0%,
100% {
transform: translate(0, 0) scale(1);
opacity: 0.8;
}
50% {
transform: translate(-12px, 14px) scale(1.05);
opacity: 0.95;
}
}
</style>
@@ -0,0 +1,57 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import ImagePlaceholder from './image-placeholder.svelte';
/**
* Heading row + full-width image preview. Renders a real image when `imageSrc` is set,
* otherwise drops back to `ImagePlaceholder` so unstaged sections still surface the gap.
*
* @typedef {Object} Props
* @property {string} eyebrow
* @property {string} title
* @property {string} [description]
* @property {string} [imageSrc] Real image path (omit to render placeholder).
* @property {string} [imageAlt]
* @property {string} [suggestedFile] Required when `imageSrc` is omitted.
* @property {string} [caption]
* @property {string} [aspect] Tailwind aspect class (default "aspect-[16/9]")
* @property {string} [bgClass] (default "bg-gray-50")
*/
/** @type {Props} */
let {
eyebrow,
title,
description,
imageSrc,
imageAlt = '',
suggestedFile,
caption = '',
aspect = 'aspect-[16/9]',
bgClass = 'bg-gray-50'
} = $props();
</script>
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 max-w-[620px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
{#if description}
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
{/if}
</div>
<BlurFade delay={0.1} once>
<div class="mx-auto max-w-[1100px]">
{#if imageSrc}
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img src={imageSrc} alt={imageAlt} class="block h-auto w-full" loading="lazy" decoding="async" />
</div>
{:else}
<ImagePlaceholder {suggestedFile} {caption} {aspect} />
{/if}
</div>
</BlurFade>
</div>
</section>
@@ -0,0 +1,29 @@
<script>
import Image from '@lucide/svelte/icons/image';
/**
* @typedef {Object} Props
* @property {string} suggestedFile Suggested static path (e.g. "/static/foo.png")
* @property {string} [caption] Short description of what should go here
* @property {string} [aspect] Tailwind aspect class (default "aspect-[4/3]")
* @property {string} [className] Extra classes
*/
/** @type {Props} */
let { suggestedFile, caption = '', aspect = 'aspect-[4/3]', className = '' } = $props();
</script>
<div
class="relative flex w-full flex-col items-center justify-center gap-3 overflow-hidden rounded-2xl border-2 border-dashed border-blue-300 bg-gradient-to-br from-blue-50 via-white to-gray-100 p-6 text-center {aspect} {className}"
>
<div
class="flex h-14 w-14 items-center justify-center rounded-2xl bg-white text-blue-700 shadow ring-1 ring-blue-100"
>
<Image size={26} strokeWidth={1.6} />
</div>
<p class="text-sm font-medium text-gray-700">Image placeholder</p>
<p class="text-xs text-gray-500">Suggested: {suggestedFile}</p>
{#if caption}
<p class="max-w-[80%] text-[11px] text-gray-400">{caption}</p>
{/if}
</div>
+25
View File
@@ -1,30 +1,55 @@
// UI Components - Named exports for easy importing
export { default as AiBuildsSection } from './ai-builds-section.svelte';
export { default as AiSection } from './ai-section.svelte';
export { default as AiTutorSection } from './ai-tutor-section.svelte';
export { default as AutomateSection } from './automate-section.svelte';
export { default as AutomationBand } from './automation-band.svelte';
export { default as BlogListItem } from './blog-list-item.svelte';
export { default as BoxedGrid } from './boxed-grid.svelte';
export { default as CertificateMock } from './certificate-mock.svelte';
export { default as CertEditorSection } from './cert-editor-section.svelte';
export { default as CertSection } from './cert-section.svelte';
export { default as CodeBlock } from './code-block.svelte';
export { default as CoreFeatureSection } from './core-feature-section.svelte';
export { default as CoreFeatures } from './core-features.svelte';
export { default as CustomImg } from './custom-img.svelte';
export { default as DeploySection } from './deploy-section.svelte';
export { default as EmbedCodePreview } from './embed-code-preview.svelte';
export { default as EnterpriseSection } from './enterprise-section.svelte';
export { default as Faq } from './faq.svelte';
export { default as FeatureGrid } from './feature-grid.svelte';
export { default as FeaturesSection } from './features-section.svelte';
export { default as Footer } from './footer.svelte';
export { default as FooterCta } from './footer-cta.svelte';
export { default as Hero } from './hero.svelte';
export { default as HeroSubtitle } from './hero-subtitle.svelte';
export { default as ImageDemoSection } from './image-demo-section.svelte';
export { default as ImagePlaceholder } from './image-placeholder.svelte';
export { default as LanguageReel } from './language-reel.svelte';
export { default as LearnerSection } from './learner-section.svelte';
export { default as Modal } from './modal.svelte';
export { default as MoreFeatures } from './more-features.svelte';
export { default as McpServerSection } from './mcp-server-section.svelte';
export { default as MultiAcademySection } from './multi-academy-section.svelte';
export { default as Navigation } from './navigation.svelte';
export { default as OssSection } from './oss-section.svelte';
export { default as NotFound } from './not-found.svelte';
export { default as PageHeader } from './page-header.svelte';
export { default as PageSignupCTA } from './page-signup-cta.svelte';
export { default as ParticleField } from './particle-field.svelte';
export { default as PillList } from './pill-list.svelte';
export { default as QuestionTypes } from './question-types.svelte';
export { default as RoiBand } from './roi-band.svelte';
export { default as SenjaEmbed } from './senja-embed.svelte';
export { default as SplitFeatureSection } from './split-feature-section.svelte';
export { default as StudentDashboard } from './student-dashboard.svelte';
export { default as Success } from './success.svelte';
export { default as Testimonial } from './testimonial.svelte';
export { default as Timeline } from './timeline.svelte';
export { default as ToolsHeader } from './tools-header.svelte';
export { default as TrustBand } from './trust-band.svelte';
export { default as UploadWidget } from './upload-widget.svelte';
export { default as VideoDemoSection } from './video-demo-section.svelte';
export { default as VideoPlaceholder } from './video-placeholder.svelte';
export { default as VsHelpDocs } from './vs-help-docs.svelte';
export { default as WorkflowSteps } from './workflow-steps.svelte';
@@ -0,0 +1,238 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import Check from '@lucide/svelte/icons/check';
import Languages from '@lucide/svelte/icons/languages';
/**
* @typedef {Object} Lang
* @property {string} code Two-letter language code (e.g. "en", "es")
* @property {string} native Native script name (e.g. "Español", "हिन्दी")
* @property {string} en English name (e.g. "Spanish", "Hindi")
* @property {string} sample Translated sample lesson title in this language
* @property {string} tone Tailwind gradient classes (e.g. "from-blue-100 to-blue-200")
* @property {string} ring Tailwind ring color class (e.g. "ring-blue-200")
*/
/**
* @typedef {Object} Props
* @property {Lang[]} langs
* @property {string} eyebrow Eyebrow label (e.g. "Global partners")
* @property {string} headlineLead Headline lead text (e.g. "Train your network")
* @property {string} headlineAccent Headline accent text shown in blue with a sweep (e.g. "in their language.")
* @property {string} description Body paragraph
* @property {string} moduleLabel Top of preview card (e.g. "Module 2 · Implementation")
* @property {string} footerLabel Bottom badge in preview card (e.g. "8 lessons" or "6 lessons · 18 min")
* @property {string} [bgClass] Optional section background (defaults to bg-gray-50)
*/
/** @type {Props} */
let {
langs,
eyebrow,
headlineLead,
headlineAccent,
description,
moduleLabel,
footerLabel,
bgClass = 'bg-gray-50'
} = $props();
</script>
<section class="relative overflow-hidden px-6 py-16 lg:px-12 lg:py-24 {bgClass}">
<div
class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_85%_40%,rgba(2,51,189,0.06),transparent_55%)]"
></div>
<div class="relative mx-auto max-w-[1100px]">
<div class="grid grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<BlurFade delay={0} once>
<div>
<div class="mb-3 inline-flex items-center gap-2 text-xs font-medium tracking-widest text-blue-700 uppercase">
<Languages size={14} />
{eyebrow}
</div>
<h2 class="text-[clamp(2rem,3vw,2.6rem)] leading-[1.15] font-medium tracking-tight">
{headlineLead}
<span class="cio-lang-headline relative inline-block">
<span class="text-blue-700">{headlineAccent}</span>
</span>
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
<!-- Inline language switcher mock -->
<div
class="mt-7 inline-flex w-full max-w-[320px] flex-col rounded-2xl border border-gray-200 bg-white p-1.5 shadow-sm"
>
<div class="px-3 py-1.5 text-[10px] font-medium tracking-widest text-gray-500 uppercase">
Lesson language
</div>
<div class="space-y-1">
{#each langs.slice(0, 4) as lang, i}
<div
class="flex items-center justify-between rounded-lg px-3 py-2 transition-colors hover:bg-gray-50 {i ===
1
? 'bg-blue-50'
: ''}"
>
<div class="flex items-center gap-3">
<span
class="flex h-6 w-6 items-center justify-center rounded-md bg-gradient-to-br {lang.tone} text-[10px] font-bold text-gray-800 ring-1 {lang.ring}"
>
{lang.code.toUpperCase()}
</span>
<span class="text-sm text-gray-800">{lang.native}</span>
<span class="text-xs text-gray-400">{lang.en}</span>
</div>
{#if i === 1}
<Check size={14} class="text-blue-700" />
{/if}
</div>
{/each}
<div class="rounded-lg px-3 py-2 text-xs text-gray-400">
+ {Math.max(0, langs.length - 4)} more languages
</div>
</div>
</div>
</div>
</BlurFade>
<!-- Animated polyglot lesson reel -->
<BlurFade delay={0.15} once>
<div class="relative">
<div
class="pointer-events-none absolute -inset-4 z-0 rounded-3xl bg-[conic-gradient(from_30deg,rgba(2,51,189,0.08),rgba(2,51,189,0.18),rgba(2,51,189,0.08))] blur-2xl"
></div>
<div class="relative rounded-2xl border border-gray-200 bg-white shadow-2xl/5">
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-3.5">
<div class="flex items-center gap-2 text-xs text-gray-500">
<span class="relative flex h-2 w-2">
<span class="absolute inline-flex h-2 w-2 animate-ping rounded-full bg-blue-500 opacity-60"></span>
<span class="relative inline-flex h-2 w-2 rounded-full bg-blue-600"></span>
</span>
{moduleLabel}
</div>
<div class="text-[10px] font-medium tracking-widest text-blue-700 uppercase">Live preview</div>
</div>
<!-- Cycling lesson card -->
<div class="relative h-[260px] overflow-hidden">
{#each langs as lang, i}
<div
class="cio-lang-cycle absolute inset-0 flex flex-col px-6 py-6"
style:--delay="{i * 2}s"
style:--total="{langs.length * 2}s"
>
<div class="mb-4 flex items-center gap-3">
<span
class="flex h-9 w-9 items-center justify-center rounded-xl bg-gradient-to-br {lang.tone} text-sm font-bold text-gray-800 ring-1 {lang.ring}"
>
{lang.code.toUpperCase()}
</span>
<div>
<p class="text-[11px] tracking-wide text-gray-400 uppercase">Now rendering · {lang.en}</p>
<p class="text-xs text-gray-600">{lang.native}</p>
</div>
</div>
<p class="text-xl leading-snug font-medium text-gray-950" dir="auto">{lang.sample}</p>
<div class="mt-5 space-y-2">
<div class="h-1.5 w-[88%] rounded-full bg-gray-100"></div>
<div class="h-1.5 w-[72%] rounded-full bg-gray-100"></div>
<div class="h-1.5 w-[60%] rounded-full bg-gray-100"></div>
</div>
<div class="mt-auto flex items-center justify-between pt-4">
<span class="rounded-full bg-blue-50 px-2.5 py-1 text-[10px] font-medium text-blue-700"
>{footerLabel}</span
>
<span class="text-[10px] text-gray-400">auto · per-learner language</span>
</div>
</div>
{/each}
</div>
<!-- Progress dots -->
<div class="border-t border-gray-100 px-5 py-3">
<div class="flex items-center justify-center gap-1.5">
{#each langs as _, i}
<span
class="cio-lang-dot h-1.5 rounded-full bg-gray-200"
style:--delay="{i * 2}s"
style:--total="{langs.length * 2}s"
></span>
{/each}
</div>
</div>
</div>
</div>
</BlurFade>
</div>
</div>
</section>
<style>
/* Each language gets a 2s window; total = langs.length × 2s, supplied via --total */
.cio-lang-cycle {
opacity: 0;
animation: cio-lang-fade var(--total, 20s) ease-in-out infinite;
animation-delay: var(--delay, 0s);
}
@keyframes cio-lang-fade {
0% {
opacity: 0;
transform: translateY(8px);
}
1% {
opacity: 1;
transform: translateY(0);
}
9% {
opacity: 1;
transform: translateY(0);
}
11% {
opacity: 0;
transform: translateY(-8px);
}
100% {
opacity: 0;
transform: translateY(-8px);
}
}
.cio-lang-dot {
width: 6px;
animation: cio-lang-dot var(--total, 20s) ease-in-out infinite;
animation-delay: var(--delay, 0s);
}
@keyframes cio-lang-dot {
0%,
11%,
100% {
background-color: rgb(229 231 235);
width: 6px;
}
1%,
9% {
background-color: rgb(29 78 216);
width: 22px;
}
}
.cio-lang-headline::before {
content: '';
position: absolute;
inset: auto 0 -2px 0;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(29, 78, 216, 0.35), transparent);
animation: cio-lang-sweep 3.5s ease-in-out infinite;
}
@keyframes cio-lang-sweep {
0%,
100% {
transform: translateX(-100%);
opacity: 0;
}
50% {
transform: translateX(100%);
opacity: 1;
}
}
</style>
@@ -1,6 +1,5 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import { BorderBeam } from '@cio/ui/custom/animation/border-beam';
const learnerModes = [
{
@@ -77,7 +76,6 @@
<!-- Lesson Mock UI -->
<BlurFade delay={0.2} once>
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-gray-50 shadow-2xl/5">
<BorderBeam size={200} duration={14} colorFrom="#0233BD" colorTo="#93c5fd" />
<div class="flex items-center justify-between border-b border-gray-200 bg-white px-5 py-4">
<div class="text-sm font-medium">Data Privacy & Compliance</div>
<div class="rounded-full border border-gray-200 bg-gray-50 px-2.5 py-0.5 text-xs text-gray-500">
@@ -0,0 +1,98 @@
<script>
import CodeBlock from './code-block.svelte';
/**
* Shared MCP server section — dark background, copy + mcp.json on left/right.
* Used on /automation and /mcp-recipes.
*
* @typedef {Object} Props
* @property {string} [eyebrow]
* @property {string} [title]
* @property {string} [description]
* @property {boolean} [showTools] Render the list of MCP tool names (default true)
*/
/** @type {Props} */
let {
eyebrow = 'MCP server',
title = 'Course authoring from your coding agent.',
description = 'Connect Cursor, Claude Code, Codex, or OpenCode to ClassroomIO and let your agent draft courses, reorganise sections, generate exercises, and publish them without leaving your terminal.',
showTools = true
} = $props();
const mcpJsonLines = [
'<span class="text-slate-500">{</span>',
' <span class="text-sky-300">"mcpServers"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"classroomio"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"command"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"npx"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"args"</span><span class="text-slate-500">: [</span><span class="text-emerald-300">"-y"</span><span class="text-slate-500">,</span> <span class="text-emerald-300">"@classroomio/mcp"</span><span class="text-slate-500">],</span>',
' <span class="text-sky-300">"env"</span><span class="text-slate-500">: { </span><span class="text-sky-300">"CLASSROOMIO_API_KEY"</span><span class="text-slate-500">: </span><span class="text-emerald-300">"&lt;your-mcp-key&gt;"</span><span class="text-slate-500"> }</span>',
' <span class="text-slate-500">}</span>',
' <span class="text-slate-500">}</span>',
'<span class="text-slate-500">}</span>'
];
const mcpTools = [
'list_org_courses',
'create_course_draft',
'create_course_draft_from_course',
'update_course_draft',
'publish_course_draft',
'publish_course_draft_to_existing_course',
'update_course_landing_page',
'create_course_exercise',
'create_course_exercise_from_template',
'update_course_exercise',
'reorder_course_content',
'tag_courses',
'tag_course_draft',
'get_course_structure',
'get_course_draft',
'get_course_exercise',
'list_course_exercises'
];
</script>
<section class="relative overflow-hidden bg-gray-950 px-6 py-12 text-white lg:px-12 lg:py-16">
<div
class="pointer-events-none absolute -top-[200px] -right-[200px] h-[500px] w-[500px] bg-[radial-gradient(circle,rgba(2,51,189,0.18)_0%,transparent_70%)]"
></div>
<div class="relative mx-auto grid max-w-[1100px] grid-cols-1 items-start gap-12 lg:grid-cols-[1fr_1fr] lg:gap-20">
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-400 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight text-white">
{title}
</h2>
<p class="mt-5 max-w-[520px] text-sm leading-relaxed text-gray-400">
{description}
</p>
{#if showTools}
<div class="mt-6">
<p class="mb-3 text-[10px] tracking-wider text-gray-500 uppercase">Available tools</p>
<div class="flex flex-wrap gap-2">
{#each mcpTools as tool}
<span
class="rounded-md border border-white/10 bg-white/[0.05] px-2.5 py-1 font-mono text-[11px] text-blue-300"
>
{tool}
</span>
{/each}
</div>
</div>
{/if}
</div>
<div class="relative">
<div class="relative overflow-hidden rounded-2xl border border-white/[0.08] bg-white/[0.03]">
<CodeBlock
fileName="mcp.json"
lang="JSON"
codeLines={mcpJsonLines}
statusBar="paste into your agent's MCP config"
/>
</div>
</div>
</div>
</section>
@@ -0,0 +1,133 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import BoxedGrid from './boxed-grid.svelte';
import Palette from '@lucide/svelte/icons/palette';
import Globe from '@lucide/svelte/icons/globe';
import Receipt from '@lucide/svelte/icons/receipt';
const academies = [
{
index: '01',
name: 'Customer Academy',
domain: 'learn.acme.com',
audience: 'Customers · onboarding · adoption',
barWidth: '72%',
dot: 'bg-blue-500',
bar: 'bg-blue-500',
badge: 'bg-blue-50 text-blue-700'
},
{
index: '02',
name: 'Partner Academy',
domain: 'partners.acme.com',
audience: 'Resellers · agencies · integrators',
barWidth: '54%',
dot: 'bg-violet-500',
bar: 'bg-violet-500',
badge: 'bg-violet-50 text-violet-700'
},
{
index: '03',
name: 'Internal Academy',
domain: 'team.acme.com',
audience: 'Employees · onboarding · compliance',
barWidth: '88%',
dot: 'bg-emerald-500',
bar: 'bg-emerald-500',
badge: 'bg-emerald-50 text-emerald-700'
}
];
const benefits = [
{
Icon: Palette,
title: 'Brand isolated',
description: 'Own logo, colors, theme. Customers never see partner content.'
},
{
Icon: Globe,
title: 'Own domain',
description: 'Point a CNAME, click verify. SSL handled.'
},
{
Icon: Receipt,
title: 'One invoice',
description: 'Add a new academy when you launch a new audience.'
}
];
</script>
<section class="bg-white px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<!-- Header: eyebrow on top, headline + supporting text in a split row -->
<BlurFade once>
<div class="text-xs font-medium tracking-widest text-blue-700 uppercase">Multi-academy</div>
</BlurFade>
<div class="mt-6 grid grid-cols-1 gap-8 lg:grid-cols-[1.4fr_1fr] lg:items-end">
<BlurFade delay={0.05} once>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight text-gray-950">
One platform,
<em class="text-gray-700 not-italic">every audience.</em>
</h2>
</BlurFade>
<BlurFade delay={0.1} once>
<p class="max-w-[360px] text-sm leading-relaxed text-gray-500 lg:text-right lg:text-base">
Three workspaces, three brands, three domains. One login. One bill.
</p>
</BlurFade>
</div>
<!-- Academies list -->
<div class="mt-10 border-t border-gray-200">
{#each academies as a, i}
<BlurFade delay={0.05 * i + 0.15} once>
<div
class="grid grid-cols-[auto_1fr] gap-x-4 border-b border-gray-200 py-5 lg:grid-cols-[40px_1.6fr_1.6fr_1fr_auto] lg:items-center lg:gap-x-6"
>
<!-- Number -->
<p class="font-mono text-xs text-gray-400">{a.index}</p>
<!-- Name + domain -->
<div class="flex items-start gap-3">
<span class="mt-2 h-2 w-2 shrink-0 rounded-full {a.dot}"></span>
<div>
<p class="text-sm text-gray-950 lg:text-base">{a.name}</p>
<p class="font-mono text-[11px] text-gray-500 lg:text-xs">{a.domain}</p>
</div>
</div>
<!-- Audience -->
<p class="col-span-2 mt-2 text-sm text-gray-600 lg:col-span-1 lg:mt-0">{a.audience}</p>
<!-- Progress bar -->
<div class="col-span-2 mt-2 h-1 w-full overflow-hidden rounded-full bg-gray-100 lg:col-span-1 lg:mt-0">
<div class="h-full rounded-full {a.bar}" style="width: {a.barWidth}"></div>
</div>
<!-- Active badge -->
<span
class="col-span-2 mt-2 inline-block w-fit rounded-full px-2.5 py-1 text-xs font-medium {a.badge} lg:col-span-1 lg:mt-0"
>
Active
</span>
</div>
</BlurFade>
{/each}
</div>
<!-- Bottom feature row -->
<div class="mt-10">
<BoxedGrid items={benefits} cols={3} padding="p-5 lg:p-6">
{#snippet cell(benefit)}
<div class="text-gray-950">
<benefit.Icon size={18} strokeWidth={1.6} />
</div>
<h3 class="mt-3 text-sm font-medium text-gray-950 lg:text-base">{benefit.title}</h3>
<p class="mt-1.5 text-xs leading-relaxed text-gray-500 lg:text-sm">{benefit.description}</p>
{/snippet}
</BoxedGrid>
</div>
</div>
</section>
@@ -1,13 +1,17 @@
<script lang="ts">
import ChevronDown from '@lucide/svelte/icons/chevron-down';
import Gamepad from '@lucide/svelte/icons/gamepad';
import Github from '@lucide/svelte/icons/github';
import Hourglass from '@lucide/svelte/icons/hourglass';
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
import LocateFixed from '@lucide/svelte/icons/locate-fixed';
import Menu from '@lucide/svelte/icons/menu';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MousePointerClick from '@lucide/svelte/icons/mouse-pointer-click';
import Server from '@lucide/svelte/icons/server';
import Sparkles from '@lucide/svelte/icons/sparkles';
import Timer from '@lucide/svelte/icons/timer';
import Webhook from '@lucide/svelte/icons/webhook';
import X from '@lucide/svelte/icons/x';
import { page } from '$app/state';
import { fly } from 'svelte/transition';
@@ -61,23 +65,50 @@
}
const solutions: NavCollectionItem[] = [
{
key: 'employee-training',
title: 'Employee Training',
subtitle: 'Keep your team in sync.',
href: '/employee-training'
},
{
key: 'bootcamps',
title: 'Bootcamps',
subtitle: 'Drive student satisfaction.',
href: '/bootcamps'
},
{
key: 'customer-education',
title: 'Customer Education',
subtitle: 'Teach customers your product.',
subtitle: 'Customer academies for SaaS.',
href: '/customer-education'
},
{
key: 'compliance-training',
title: 'Compliance Training',
subtitle: 'Turn policies and SOPs into proof.',
href: '/compliance-training'
},
{
key: 'partner-training',
title: 'Partner Training',
subtitle: 'A separate academy for your channel.',
href: '/partner-training'
}
];
const developers: NavCollectionItem[] = [
{
key: 'automation',
title: 'Automation',
subtitle: 'API, Webhooks, and MCP server.',
href: '/automation'
},
{
key: 'self-hosting',
title: 'Self-hosting & Docs',
subtitle: 'Run ClassroomIO in your own VPC.',
href: 'https://docs.classroomio.com'
},
{
key: 'mcp-recipes',
title: 'MCP Recipes',
subtitle: 'Ready-to-run agent prompts.',
href: '/mcp-recipes'
},
{
key: 'github',
title: 'GitHub',
subtitle: 'Star, read the source, fork.',
href: '/github'
}
];
@@ -121,6 +152,12 @@
href: '/',
items: solutions
},
{
key: 'developers',
title: 'Developers',
href: '/automation',
items: developers
},
{
key: 'free-tools',
title: 'Free Tools',
@@ -140,6 +177,7 @@
];
let isSolutionsActive = $derived(!!solutions.some((s) => activeLink.includes(s.key)));
let isDevelopersActive = $derived(!!developers.some((d) => activeLink.includes(d.key)));
let isFreeToolsActive = $derived(activeLink.startsWith('/tools'));
function isNavItemActive(navItem: NavItem) {
@@ -147,6 +185,10 @@
return isSolutionsActive;
}
if (navItem.key === 'developers') {
return isDevelopersActive;
}
if (navItem.key === 'free-tools') {
return isFreeToolsActive;
}
@@ -165,12 +207,20 @@
{...restProps}
>
<div class="shrink-0">
{#if key === 'employee-training'}
{#if key === 'compliance-training'}
<LibraryBigIcon size={24} />
{:else if key === 'bootcamps'}
{:else if key === 'partner-training'}
<LocateFixed size={24} />
{:else if key === 'customer-education'}
<MessagesSquare size={24} />
{:else if key === 'automation'}
<Webhook size={24} />
{:else if key === 'self-hosting'}
<Server size={24} />
{:else if key === 'mcp-recipes'}
<Sparkles size={24} />
{:else if key === 'github'}
<Github size={24} />
{:else if key === 'progress'}
<LoaderCircle size={24} />
{:else if key === 'pomodoro'}
@@ -206,7 +256,7 @@
</div>
</a>
<section class="hidden lg:block">
<section class="hidden! lg:block!">
<NavigationMenu.Root class="p-1">
<NavigationMenu.List class="flex w-full items-center justify-center gap-1">
{#each navItems as navItem}
@@ -253,7 +303,7 @@
</NavigationMenu.Root>
</section>
<div class="hidden flex-row items-center justify-between gap-3 lg:flex">
<div class="hidden! flex-row items-center justify-between gap-3 lg:flex!">
<a href="/discord" target="_blank" class="flex items-center transition-opacity duration-200 hover:opacity-80">
<img loading="lazy" alt="discord logo" src="/discord-blue.png" class="h-5 w-6 cursor-pointer" />
</a>
@@ -284,7 +334,7 @@
</a>
</div>
<button type="button" aria-label="Hamburger Menu" class="shrink-0 lg:hidden" onclick={handleShowSolutions}>
<button type="button" aria-label="Hamburger Menu" class="shrink-0 lg:hidden!" onclick={handleShowSolutions}>
<Menu size={24} />
</button>
@@ -0,0 +1,85 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import Github from '@lucide/svelte/icons/github';
import Server from '@lucide/svelte/icons/server';
import Coins from '@lucide/svelte/icons/coins';
/** @type {{ stars?: number }} */
let { stars = 0 } = $props();
const lanes = [
{
Icon: Github,
title: 'AGPL on GitHub',
description: 'Dashboard, API, AI assistant, MCP server. All of it.'
},
{
Icon: Server,
title: 'Self-host or hosted',
description: 'Run it in your VPC, or let us run it. Same product.'
},
{
Icon: Coins,
title: 'No per-seat fees',
description: 'Pricing scales with workspaces, not headcount.'
}
];
</script>
<section class="bg-white px-6 py-16 lg:px-12 lg:py-24">
<div class="mx-auto max-w-[1100px]">
<div class="mb-12 grid grid-cols-1 gap-12 lg:grid-cols-[1fr_1fr] lg:items-end lg:gap-20">
<BlurFade once>
<div>
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">Open source</div>
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">
Read it. Fork it. Run it.
</h2>
</div>
</BlurFade>
<BlurFade delay={0.1} once>
<p class="text-base leading-relaxed text-gray-500 lg:text-lg">
AGPL on GitHub. Self-host or use ours. Bring your own AI keys.
</p>
</BlurFade>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
{#each lanes as lane, i}
<BlurFade delay={0.08 * i} once>
<div class="h-full rounded-2xl border border-gray-200 bg-gray-50 p-6">
<div
class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-white text-blue-700 ring-1 ring-gray-200"
>
<lane.Icon size={20} strokeWidth={1.75} />
</div>
<h3 class="text-base font-medium text-gray-950">{lane.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-gray-500">{lane.description}</p>
</div>
</BlurFade>
{/each}
</div>
<div class="mt-8 flex flex-wrap items-center gap-3">
<a
href="/github"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg bg-gray-950 px-5 py-2.5 text-sm font-medium text-white no-underline transition-all hover:-translate-y-0.5 hover:bg-blue-700"
>
<Github size={16} />
Star on GitHub
{#if stars}<span class="text-blue-300">{stars}</span>{/if}
</a>
<a
href="https://docs.classroomio.com/self-hosting"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-950 no-underline transition-all hover:border-gray-950"
>
Self-hosting guide →
</a>
</div>
</div>
</section>
@@ -14,7 +14,7 @@
<header class="relative w-full overflow-hidden border-b border-slate-100 pt-32 pb-16 lg:min-h-[20rem]">
<DotField
dotRadius={1.5}
dotSpacing={14}
dotSpacing={15}
cursorRadius={420}
bulgeOnly={true}
bulgeStrength={56}
@@ -1,5 +1,6 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
import { Waves } from '@cio/ui/custom/animation/waves';
interface Props {
header?: string;
@@ -13,19 +14,40 @@
</script>
<div
class="floating grad relative mx-auto my-5 flex h-[61vh] w-full max-w-4xl flex-col items-center justify-center space-y-8 px-6 py-12 text-center shadow-2xl lg:h-[50vh] lg:w-[70%] lg:rounded-3xl"
class="relative mx-auto my-5 flex h-[61vh] w-full max-w-4xl flex-col items-center justify-center overflow-hidden bg-gray-950 px-6 py-12 text-center shadow-2xl lg:h-[50vh] lg:w-[70%] lg:rounded-3xl"
>
<div>
<h3 class="font-display mt-3 max-w-2xl text-xl tracking-tight text-white lg:mt-2 lg:text-4xl">
<div
class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_100%,rgba(2,51,189,0.92)_0%,rgba(4,15,45,0.98)_58%,rgb(3,7,18)_100%)]"
></div>
<Waves
lineColor="rgba(255,255,255,0.18)"
class="ui:pointer-events-none ui:absolute ui:inset-0 ui:h-full ui:w-full"
xGap={14}
yGap={34}
waveAmpX={24}
waveAmpY={12}
maxCursorMove={72}
/>
<div
class="pointer-events-none absolute inset-0 bg-linear-to-t from-gray-950/40 via-transparent to-white/[0.04]"
></div>
<div class="relative z-10 flex w-full flex-col items-center text-center">
<h3 class="font-display mx-auto mt-3 max-w-2xl text-center text-xl tracking-tight text-white lg:mt-2 lg:text-4xl">
{header}
</h3>
<p class="text-md mt-6 text-gray-300">
<p class="text-md mx-auto mt-6 max-w-2xl text-center text-gray-300">
{subText}
</p>
</div>
{#if demo}
<Button variant="ghost" data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo" class="bg-white!">
<Button
variant="ghost"
data-cal-config="'layout':'month_view'"
data-cal-link="classroomio/demo"
class="relative z-10 mt-8 bg-white!"
>
{btnLabel}
</Button>
{:else}
@@ -33,21 +55,10 @@
href={link}
variant="link"
target="_blank"
class="ui:bg-secondary ui:text-secondary-foreground"
class="ui:bg-secondary ui:text-secondary-foreground relative z-10 mt-8"
rel="noopener noreferrer nofollow"
>
{btnLabel}
</Button>
{/if}
</div>
<style>
.grad {
/* background: rgb(2, 51, 189);
background: radial-gradient(circle at 50% 100%, rgba(2, 51, 189, 1) 1%, rgb(4, 2, 56) 100%); */
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
background-image: url('https://brand.cdn.clsrio.com/cta-bg.png');
}
</style>
@@ -0,0 +1,242 @@
<script>
import { onMount } from 'svelte';
/**
* Reusable animated particle cloud, same family as the hero.
*
* @typedef {Object} ClusterSpec
* @property {number} x Normalized 0–1 x position
* @property {number} y Normalized 0–1 y position
* @property {number} rx
* @property {number} ry
* @property {number} density
* @property {number} warmth 0 (cool) → 1 (warm/brand blue)
*
* @typedef {Object} Props
* @property {ClusterSpec[]} [clusters] Density centers. Defaults to the hero's 4-cluster diagonal.
* @property {number} [intensity] Multiplier for particle count (default 1)
* @property {boolean} [highlights] Render glowing twinkle stars + constellation lines (default true)
* @property {string} [class] Extra classes for the canvas element
*/
/** @type {Props} */
let {
clusters: clusterSpec = [
{ x: 0.3, y: 0.72, rx: 90, ry: 130, density: 0.55, warmth: 0.1 },
{ x: 0.52, y: 0.55, rx: 150, ry: 100, density: 0.95, warmth: 0.38 },
{ x: 0.74, y: 0.38, rx: 190, ry: 150, density: 1.25, warmth: 0.78 },
{ x: 0.92, y: 0.22, rx: 120, ry: 85, density: 0.7, warmth: 1.0 }
],
intensity = 1,
highlights: showHighlights = true,
class: className = ''
} = $props();
let canvas;
onMount(() => {
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = Math.min(window.devicePixelRatio || 1, 2);
let w = 0;
let h = 0;
let particles = [];
let highlights = [];
let clusters = [];
let rafId;
let time = 0;
function warmRGB(wm) {
const r = Math.round(110 - wm * 80);
const g = Math.round(130 - wm * 50);
const b = Math.round(180 + wm * 50);
return [r, g, b];
}
function gaussian2d(rx, ry) {
const u1 = Math.max(Math.random(), 1e-6);
const u2 = Math.random();
const r = Math.sqrt(-2 * Math.log(u1));
const t = 2 * Math.PI * u2;
return [r * Math.cos(t) * rx, r * Math.sin(t) * ry];
}
function resize() {
const rect = canvas.getBoundingClientRect();
w = rect.width;
h = rect.height;
canvas.width = Math.floor(w * dpr);
canvas.height = Math.floor(h * dpr);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
seed();
}
function seed() {
particles = [];
highlights = [];
// Scale cluster sizes (and therefore particle counts) with viewport width.
// Reference width is 1100px (the content max-width). Mobile shrinks to ~50%, never bigger than 1×.
const scale = Math.max(0.5, Math.min(1, w / 1100));
clusters = clusterSpec.map((c) => ({
x: c.x * w,
y: c.y * h,
rx: c.rx * scale,
ry: c.ry * scale,
density: c.density,
warmth: c.warmth
}));
for (const c of clusters) {
const count = Math.floor(((c.rx * c.ry) / 18) * c.density * intensity);
for (let i = 0; i < count; i++) {
const [dx, dy] = gaussian2d(c.rx * 0.34, c.ry * 0.34);
const z = Math.random();
let r, a;
if (z < 0.32) {
r = 0.3 + Math.random() * 0.6;
a = 0.1 + Math.random() * 0.18;
} else if (z < 0.85) {
r = 0.55 + Math.random() * 1.05;
a = 0.28 + Math.random() * 0.38;
} else {
r = 1.0 + Math.random() * 1.6;
a = 0.5 + Math.random() * 0.45;
}
particles.push({
bx: c.x + dx,
by: c.y + dy,
r,
a,
warmth: Math.min(1, Math.max(0, c.warmth + (Math.random() - 0.5) * 0.18)),
phase: Math.random() * Math.PI * 2,
speed: 0.3 + Math.random() * 0.7,
amp: 0.6 + Math.random() * 1.8
});
}
}
const fringeCount = Math.floor(w * 0.18 * intensity);
for (let i = 0; i < fringeCount; i++) {
const c = clusters[1 + (i % Math.max(1, clusters.length - 1))] || clusters[0];
const [dx, dy] = gaussian2d(c.rx * 1.4, c.ry * 1.4);
particles.push({
bx: c.x + dx,
by: c.y + dy,
r: 0.25 + Math.random() * 0.4,
a: 0.06 + Math.random() * 0.15,
warmth: c.warmth,
phase: Math.random() * Math.PI * 2,
speed: 0.2 + Math.random() * 0.4,
amp: 0.8 + Math.random() * 1.6
});
}
if (showHighlights) {
const starCount = 38;
for (let i = 0; i < starCount; i++) {
const c = clusters[Math.max(0, clusters.length - 2) + (i % 2)] || clusters[0];
const [dx, dy] = gaussian2d(c.rx * 0.45, c.ry * 0.45);
highlights.push({
x: c.x + dx,
y: c.y + dy,
r: 1.2 + Math.random() * 1.6,
phase: Math.random() * Math.PI * 2,
speed: 0.8 + Math.random() * 0.7,
warmth: c.warmth
});
}
}
}
function draw() {
ctx.clearRect(0, 0, w, h);
time += 0.0035;
ctx.globalCompositeOperation = 'source-over';
for (const c of clusters) {
const radius = Math.max(c.rx, c.ry) * 1.7;
const [r, g, b] = warmRGB(c.warmth);
const grd = ctx.createRadialGradient(c.x, c.y, 0, c.x, c.y, radius);
grd.addColorStop(0, `rgba(${r},${g},${b},${0.1 * c.density})`);
grd.addColorStop(0.45, `rgba(${r},${g},${b},${0.04 * c.density})`);
grd.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grd;
ctx.fillRect(c.x - radius, c.y - radius, radius * 2, radius * 2);
}
for (let i = 0; i < particles.length; i++) {
const p = particles[i];
const dx = Math.sin(time * p.speed + p.phase) * p.amp;
const dy = Math.cos(time * p.speed * 0.65 + p.phase) * p.amp * 0.7;
const x = p.bx + dx;
const y = p.by + dy;
const [r, g, b] = warmRGB(p.warmth);
ctx.beginPath();
ctx.arc(x, y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `rgba(${r},${g},${b},${p.a})`;
ctx.fill();
}
if (showHighlights) {
ctx.lineWidth = 0.55;
for (let i = 0; i < highlights.length; i++) {
const a = highlights[i];
for (let j = i + 1; j < highlights.length; j++) {
const b = highlights[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const d2 = dx * dx + dy * dy;
if (d2 < 12000) {
const d = Math.sqrt(d2);
const alpha = (1 - d / 110) * 0.22;
if (alpha > 0) {
const wm = (a.warmth + b.warmth) * 0.5;
const [rr, gg, bb] = warmRGB(wm);
ctx.strokeStyle = `rgba(${rr},${gg},${bb},${alpha})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
}
}
ctx.globalCompositeOperation = 'screen';
for (const hl of highlights) {
const tw = 0.55 + 0.45 * Math.sin(time * hl.speed + hl.phase);
const [r, g, b] = warmRGB(hl.warmth);
const haloR = hl.r * 7.5;
const grd = ctx.createRadialGradient(hl.x, hl.y, 0, hl.x, hl.y, haloR);
grd.addColorStop(0, `rgba(${r},${g},${b},${0.55 * tw})`);
grd.addColorStop(0.35, `rgba(${r},${g},${b},${0.13 * tw})`);
grd.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grd;
ctx.beginPath();
ctx.arc(hl.x, hl.y, haloR, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(hl.x, hl.y, hl.r * (0.85 + 0.3 * tw), 0, Math.PI * 2);
ctx.fillStyle = `rgba(225,236,255,${0.9 * tw})`;
ctx.fill();
}
ctx.globalCompositeOperation = 'source-over';
}
rafId = requestAnimationFrame(draw);
}
resize();
draw();
window.addEventListener('resize', resize);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener('resize', resize);
};
});
</script>
<canvas bind:this={canvas} class="pointer-events-none {className}" aria-hidden="true"></canvas>
@@ -0,0 +1,62 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
/**
* Wrap-flow list of pill-shaped bordered cards with an optional colored dot.
* Used on solution pages for use-case lists, audience lists, etc.
*
* @typedef {Object} Props
* @property {string[]} items
* @property {boolean} [showDot] Show a small pastel dot at the start of each pill (default true)
* @property {boolean} [animate] Wrap each pill in BlurFade (default true)
* @property {string} [gap] Gap class between pills (default "gap-3")
* @property {string} [size] Padding/text size variant: "default" | "compact" (default "default")
*/
/** @type {Props} */
let { items, showDot = true, animate = true, gap = 'gap-3', size = 'default' } = $props();
const dotColors = [
'bg-blue-300',
'bg-emerald-300',
'bg-amber-300',
'bg-violet-300',
'bg-rose-300',
'bg-cyan-300',
'bg-orange-300',
'bg-fuchsia-300'
];
const sizes = {
default: 'px-4 py-2 text-sm',
compact: 'px-3 py-1.5 text-xs'
};
</script>
<div class="flex flex-wrap {gap}">
{#each items as item, i}
{#if animate}
<BlurFade delay={0.03 * i} once>
<span
class="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white text-gray-800 {sizes[
size
]}"
>
{#if showDot}
<span class="h-2 w-2 rounded-full {dotColors[i % dotColors.length]}"></span>
{/if}
{item}
</span>
</BlurFade>
{:else}
<span
class="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white text-gray-800 {sizes[size]}"
>
{#if showDot}
<span class="h-2 w-2 rounded-full {dotColors[i % dotColors.length]}"></span>
{/if}
{item}
</span>
{/if}
{/each}
</div>
@@ -0,0 +1,47 @@
<script>
import BoxedGrid from './boxed-grid.svelte';
/**
* @typedef {Object} Props
* @property {string} [bgClass]
* @property {string} [eyebrow]
* @property {string} [heading]
* @property {string} [subhead]
*/
/** @type {Props} */
let {
bgClass = 'bg-white',
eyebrow = 'The case for an academy',
heading = 'Customers who learn the product, stay.',
subhead = 'Numbers from teams who built one:'
} = $props();
const stats = [
{ value: '+22%', label: 'Higher retention', source: 'Forrester · customer-education TEI' },
{ value: '+38%', label: 'Product adoption', source: 'Intellum / Forrester benchmarks' },
{ value: '−6.1%', label: 'Support costs', source: 'TSIA · education services impact' }
];
</script>
<section class="{bgClass} px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-8 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{heading}</h2>
<p class="mt-3 text-sm leading-relaxed text-gray-500">{subhead}</p>
</div>
<BoxedGrid items={stats} cols={3} padding="px-5 py-10 lg:py-12">
{#snippet cell(stat)}
<div class="flex h-full flex-col items-center justify-center text-center">
<p class="text-[clamp(2rem,3.4vw,2.8rem)] font-medium tracking-tight text-gray-950">{stat.value}</p>
<p class="mt-1.5 text-sm text-gray-600">{stat.label}</p>
<p class="mt-3 text-[11px] text-gray-400">{stat.source}</p>
</div>
{/snippet}
</BoxedGrid>
<p class="mt-4 text-xs text-gray-400">Industry benchmarks. Your mileage will vary.</p>
</div>
</section>
@@ -0,0 +1,50 @@
<script>
/**
* Two-column feature split — text + optional bullets on one side, image on the other.
* Replaces bespoke per-page splits so we keep grid + spacing consistent across marketing pages.
*
* @typedef {Object} Props
* @property {string} [eyebrow]
* @property {string} title
* @property {string} description
* @property {string} imageSrc
* @property {string} imageAlt
* @property {string} [imageAspect] Tailwind aspect class (default "aspect-[4/3]")
* @property {'left'|'right'} [imagePosition] (default "right")
* @property {string} [bgClass] Section background (default "bg-white")
* @property {import('svelte').Snippet} [bullets] Snippet rendering `<li>` children
*/
/** @type {Props} */
let {
eyebrow,
title,
description,
imageSrc,
imageAlt,
imageAspect = 'aspect-[4/3]',
imagePosition = 'right',
bgClass = 'bg-white',
bullets
} = $props();
</script>
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div class={imagePosition === 'left' ? 'lg:order-2' : ''}>
{#if eyebrow}
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
{/if}
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
{#if bullets}
<ul class="mt-6 space-y-3 text-sm text-gray-700">
{@render bullets()}
</ul>
{/if}
</div>
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200 {imageAspect}">
<img src={imageSrc} alt={imageAlt} class="h-full w-full object-cover" loading="lazy" />
</div>
</div>
</section>
@@ -2,13 +2,13 @@
import { SenjaEmbed } from '$lib/components';
</script>
<section id="testimonial" class="bg-white px-6 py-16 lg:px-12 lg:py-24">
<section id="testimonial" class="bg-white px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px] text-center">
<div class="mb-3.5 text-xs font-bold tracking-widest text-blue-700 uppercase">Testimonials</div>
<h2 class="mb-4 text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-bold tracking-tight">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Testimonials</div>
<h2 class="mb-3 text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Hear from happy ClassroomIO users
</h2>
<p class="mx-auto mb-10 max-w-[520px] text-base leading-relaxed text-gray-500 lg:text-lg">
<p class="mx-auto mb-8 max-w-[520px] text-sm leading-relaxed text-gray-500">
See what excites them about building and managing training programs with ClassroomIO.
</p>
@@ -0,0 +1,22 @@
<script>
/**
* @typedef {Object} Props
* @property {string} caption Eyebrow caption (e.g. "For partner programs at")
* @property {string[]} items Items shown pipe-separated
*/
/** @type {Props} */
let { caption, items } = $props();
</script>
<section class="border-b border-gray-100 bg-white px-6 py-10 lg:px-12">
<div class="mx-auto max-w-[1100px]">
<p class="mb-6 text-center text-xs font-medium tracking-widest text-gray-500 uppercase">{caption}</p>
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-sm font-medium text-gray-500">
{#each items as item, i}
{#if i > 0}<span class="text-gray-300">·</span>{/if}
<span>{item}</span>
{/each}
</div>
</div>
</section>
@@ -0,0 +1,34 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import VideoPlaceholder from './video-placeholder.svelte';
/**
* @typedef {Object} Props
* @property {string} eyebrow Eyebrow label (e.g. "A live walkthrough")
* @property {string} title Section heading
* @property {string} [description] Lead paragraph below the heading
* @property {string} suggestedFile Path passed through to VideoPlaceholder
* @property {string} [bgClass] Optional section background (default bg-gray-50)
*/
/** @type {Props} */
let { eyebrow, title, description, suggestedFile, bgClass = 'bg-gray-50' } = $props();
</script>
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 max-w-[620px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
{#if description}
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
{/if}
</div>
<BlurFade delay={0.1} once>
<div class="relative mx-auto max-w-[920px]">
<VideoPlaceholder {suggestedFile} />
</div>
</BlurFade>
</div>
</section>
@@ -0,0 +1,30 @@
<script>
import PlayCircle from '@lucide/svelte/icons/play-circle';
/**
* @typedef {Object} Props
* @property {string} suggestedFile Suggested static path (e.g. "/compliance-demo.mp4")
* @property {string} [caption] Optional short description
* @property {string} [className] Extra classes
*/
/** @type {Props} */
let { suggestedFile, caption = '', className = '' } = $props();
</script>
<div
class="relative aspect-video w-full overflow-hidden rounded-2xl border-2 border-dashed border-blue-300 bg-gradient-to-br from-blue-50 via-white to-gray-100 {className}"
>
<div class="absolute inset-0 flex flex-col items-center justify-center gap-3 text-center">
<div
class="flex h-20 w-20 items-center justify-center rounded-full bg-white text-blue-700 shadow-lg ring-1 ring-blue-100"
>
<PlayCircle size={48} strokeWidth={1.4} />
</div>
<p class="text-sm font-medium text-gray-700">Video placeholder</p>
<p class="text-xs text-gray-500">Replace with HeroVideoDialog · suggested file: {suggestedFile}</p>
{#if caption}
<p class="max-w-[80%] text-[11px] text-gray-400">{caption}</p>
{/if}
</div>
</div>
@@ -0,0 +1,146 @@
<script>
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import Check from '@lucide/svelte/icons/check';
import X from '@lucide/svelte/icons/x';
/**
* @typedef {Object} Props
* @property {string} [bgClass]
* @property {string} [eyebrow]
* @property {string} [heading]
* @property {string} [subhead]
* @property {boolean} [showHeading]
*/
/** @type {Props} */
let {
bgClass = 'bg-white',
eyebrow = 'Help docs are not an academy',
heading = 'Reading docs isn’t the same as learning.',
subhead = 'Intercom Articles, Zendesk Guide, and Confluence are great at one thing — answering "how do I do X?" when the user already knows what they need. They were never built to teach.',
showHeading = true
} = $props();
const rows = [
{
capability: 'Sequenced learning path',
docs: false,
academy: true,
note: 'A course goes from "what this product is" to "I can do this unsupervised." Docs are alphabetical, not sequential.'
},
{
capability: 'Assessment & quiz grading',
docs: false,
academy: true,
note: 'You can’t prove someone understood an article. You can prove they passed a 12-question final.'
},
{
capability: 'Certificates & verifiable IDs',
docs: false,
academy: true,
note: 'A shareable cert turns power-users into a public community. Help docs don’t.'
},
{
capability: 'Programs / multi-course paths',
docs: false,
academy: true,
note: 'Onboarding week, admin track, power-user path — three audiences, three sequenced journeys, one academy.'
},
{
capability: 'AI tutor inside the lesson',
docs: false,
academy: true,
note: 'Lesson-aware Claude tutor answers follow-ups before the learner bounces to a support ticket.'
},
{
capability: 'Per-learner progress & analytics',
docs: false,
academy: true,
note: 'See who finished, who stalled, where they got stuck. Page-view analytics don’t answer "did this customer learn?"'
},
{
capability: 'Branded portal on your domain',
docs: true,
academy: true,
note: 'Both can. Help-doc tools usually charge extra for it. ClassroomIO doesn’t.'
},
{
capability: 'Search & quick lookup',
docs: true,
academy: false,
note: 'When someone already knows what they need, docs win. Pair an academy with your docs — don’t replace them.'
},
{
capability: 'In-product contextual answers',
docs: true,
academy: false,
note: 'Help widgets in-app are still where short-form, "right now" answers belong.'
}
];
</script>
<section class="{bgClass} px-6 py-16 lg:px-12 lg:py-24">
<div class="mx-auto max-w-[1100px]">
{#if showHeading}
<div class="mb-12 max-w-[680px]">
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(2rem,3vw,2.8rem)] leading-[1.15] font-medium tracking-tight">{heading}</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">{subhead}</p>
</div>
{/if}
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white">
<div
class="grid grid-cols-[1.6fr_repeat(2,minmax(0,0.8fr))_1.4fr] gap-0 border-b border-gray-200 bg-gray-50 px-5 py-3.5 text-[11px] font-medium tracking-widest text-gray-500 uppercase"
>
<div>Capability</div>
<div class="text-center">Help docs</div>
<div class="text-center">Company academy</div>
<div class="hidden lg:block">Why it matters</div>
</div>
{#each rows as row, i}
<BlurFade delay={0.03 * i} once>
<div
class="grid grid-cols-[1.6fr_repeat(2,minmax(0,0.8fr))_1.4fr] items-start gap-0 border-b border-gray-100 px-5 py-4 text-sm last:border-b-0 hover:bg-blue-50/30"
>
<div class="font-medium text-gray-950">{row.capability}</div>
<div class="flex justify-center">
{#if row.docs}
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-50 text-emerald-600">
<Check size={16} strokeWidth={2.5} />
</span>
{:else}
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 text-gray-400">
<X size={16} strokeWidth={2.5} />
</span>
{/if}
</div>
<div class="flex justify-center">
{#if row.academy}
<span
class="flex h-7 w-7 items-center justify-center rounded-full bg-blue-50 text-blue-700 ring-1 ring-blue-200"
>
<Check size={16} strokeWidth={2.5} />
</span>
{:else}
<span class="flex h-7 w-7 items-center justify-center rounded-full bg-gray-100 text-gray-400">
<X size={16} strokeWidth={2.5} />
</span>
{/if}
</div>
<div class="hidden text-xs leading-relaxed text-gray-500 lg:block">{row.note}</div>
</div>
</BlurFade>
{/each}
</div>
<p class="mt-6 text-xs text-gray-500">
Compared against typical capabilities of Intercom Articles, Zendesk Guide, and Confluence at time of writing. Run
your own academy alongside your existing help center — most teams who launch a customer academy keep both.
</p>
</div>
</section>
@@ -0,0 +1,113 @@
<script>
/**
* @typedef {Object} Step
* @property {string} title
* @property {string} description
* @property {string} [label] Optional small uppercase label shown below the description (e.g. "STEP 01")
*/
/**
* @typedef {Object} Props
* @property {string} eyebrow Eyebrow label (e.g. "How it works")
* @property {string} title Section heading
* @property {string} [description] Optional lead paragraph below the heading
* @property {Step[]} steps Workflow steps (typically 4)
* @property {string} [bgClass] Optional section background (default bg-white)
*/
/** @type {Props} */
let { eyebrow, title, description, steps, bgClass = 'bg-white' } = $props();
let hoveredIndex = $state(-1);
function isActive(index) {
if (hoveredIndex === -1) return index === 0;
return index <= hoveredIndex;
}
</script>
<section class="px-6 py-12 lg:px-12 lg:py-16 {bgClass}">
<div class="mx-auto max-w-[1100px]">
<div class="max-w-[680px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">{eyebrow}</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">{title}</h2>
{#if description}
<p class="mt-5 text-base leading-relaxed text-gray-500">{description}</p>
{/if}
</div>
<!-- Desktop: horizontal timeline -->
<div class="mt-12 hidden! md:block!">
<div class="relative">
<!-- connecting line -->
<div class="absolute top-[7px] right-2 left-2 h-px bg-gray-200"></div>
<div
class="relative grid gap-8"
style:grid-template-columns="repeat({steps.length}, minmax(0, 1fr))"
onmouseleave={() => (hoveredIndex = -1)}
role="presentation"
>
{#each steps as step, i}
<div class="relative" onmouseenter={() => (hoveredIndex = i)} role="presentation">
<span
class="relative z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full transition-all duration-200 {isActive(
i
)
? 'bg-blue-700 ring-4 ring-blue-100'
: 'bg-gray-300'}"
>
{#if i === 0 && hoveredIndex === -1}
<span class="absolute inline-flex h-3.5 w-3.5 animate-ping rounded-full bg-blue-500 opacity-60"
></span>
{/if}
</span>
<div class="mt-6">
<h3 class="text-sm font-medium text-gray-950">{step.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-gray-500">{step.description}</p>
<p
class="mt-4 font-mono text-[10px] tracking-[0.15em] uppercase transition-colors duration-200 {isActive(
i
)
? 'text-blue-700'
: 'text-gray-400'}"
>
{step.label ?? `Step ${String(i + 1).padStart(2, '0')}`}
</p>
</div>
</div>
{/each}
</div>
</div>
</div>
<!-- Mobile: vertical timeline -->
<div class="mt-10 md:hidden!">
<div class="relative space-y-8 pl-6" onmouseleave={() => (hoveredIndex = -1)} role="presentation">
<div class="absolute top-2 bottom-2 left-[6px] w-px bg-gray-200"></div>
{#each steps as step, i}
<div class="relative" onmouseenter={() => (hoveredIndex = i)} role="presentation">
<span
class="absolute top-1 -left-6 flex h-3 w-3 items-center justify-center rounded-full transition-all duration-200 {isActive(
i
)
? 'bg-blue-700 ring-4 ring-blue-100'
: 'bg-gray-300'}"
></span>
<h3 class="text-sm font-medium text-gray-950">{step.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-gray-500">{step.description}</p>
<p
class="mt-3 font-mono text-[10px] tracking-[0.15em] uppercase transition-colors duration-200 {isActive(i)
? 'text-blue-700'
: 'text-gray-400'}"
>
{step.label ?? `Step ${String(i + 1).padStart(2, '0')}`}
</p>
</div>
{/each}
</div>
</div>
</div>
</section>
@@ -1,81 +0,0 @@
<script>
import { Button } from '@cio/ui/base/button';
import * as Field from '@cio/ui/base/field';
import { Input } from '@cio/ui/base/input';
import { Textarea } from '@cio/ui/base/textarea';
import { isFormValid } from '$lib/utils/is-form-valid';
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = $state('');
let email = $state('');
let title = $state('');
let description = $state('');
let isLoading = $state(false);
let errorMessage = $state('');
let sent = $state(false);
const handleClick = async () => {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[FEEDBACK] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
};
</script>
<div>
{#if sent}
<Success />
{:else}
<div class="my-10 w-full space-y-5">
<Field.Group>
<div class="flex flex-col gap-10 md:flex-row">
<Field.Field class="w-full">
<Field.Label for="feedback-name">Your Name</Field.Label>
<Input id="feedback-name" bind:value={name} required />
</Field.Field>
<Field.Field class="w-full">
<Field.Label for="feedback-email">Your Email</Field.Label>
<Input id="feedback-email" type="email" bind:value={email} required />
</Field.Field>
</div>
<Field.Field>
<Field.Label for="feedback-subject">Subject</Field.Label>
<Input id="feedback-subject" bind:value={title} required />
</Field.Field>
<Field.Field>
<Field.Label for="feedback-message">Message</Field.Label>
<Textarea id="feedback-message" bind:value={description} required />
</Field.Field>
{#if !!errorMessage}
<Field.Field>
<Field.Error>{errorMessage}</Field.Error>
</Field.Field>
{/if}
</Field.Group>
<Button onclick={handleClick} disabled={isLoading} loading={isLoading}>Submit Feedback</Button>
</div>
{/if}
</div>
@@ -1,81 +0,0 @@
<script>
import { Button } from '@cio/ui/base/button';
import * as Field from '@cio/ui/base/field';
import { Input } from '@cio/ui/base/input';
import { Textarea } from '@cio/ui/base/textarea';
import { isFormValid } from '$lib/utils/is-form-valid';
import { submitForm } from '$lib/utils/submit-form';
import { Success } from '$lib/components';
let name = $state('');
let email = $state('');
let title = $state('');
let description = $state('');
let isLoading = $state(false);
let errorMessage = $state('');
let sent = $state(false);
async function handleClick() {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[HELP] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
}
</script>
<div>
{#if sent}
<Success />
{:else}
<div class="my-10 w-full space-y-5">
<Field.Group>
<div class="flex flex-col gap-10 md:flex-row">
<Field.Field class="w-full">
<Field.Label for="help-name">Your Name</Field.Label>
<Input id="help-name" bind:value={name} required />
</Field.Field>
<Field.Field class="w-full">
<Field.Label for="help-email">Your Email</Field.Label>
<Input id="help-email" type="email" bind:value={email} required />
</Field.Field>
</div>
<Field.Field>
<Field.Label for="help-subject">Subject</Field.Label>
<Input id="help-subject" bind:value={title} required />
</Field.Field>
<Field.Field>
<Field.Label for="help-message">Message</Field.Label>
<Textarea id="help-message" bind:value={description} required />
</Field.Field>
{#if !!errorMessage}
<Field.Field>
<Field.Error>{errorMessage}</Field.Error>
</Field.Field>
{/if}
</Field.Group>
<Button onclick={handleClick} disabled={isLoading} loading={isLoading}>Submit Request</Button>
</div>
{/if}
</div>
-20
View File
@@ -1,20 +0,0 @@
export const submitForm = async (formData: any) => {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Failed to submit the form');
}
const data = await response.json();
console.log('Response from API:', data);
} catch (error) {
console.error('Error submitting form:', error);
}
};
-9
View File
@@ -24,12 +24,3 @@ export type OssFriend = {
description: string;
href: string;
};
export type FORM_TYPE_KEY = keyof typeof FORM_TYPE;
export enum FORM_TYPE {
BUG = 'BUG',
HELP = 'HELP',
FEATURE = 'FEATURE',
FEEDBACK = 'FEEDBACK'
}
@@ -1,208 +0,0 @@
<script>
import { PageSignupCTA, PageHeader } from '$lib/components';
import { Button } from '@cio/ui/base/button';
import { Badge } from '@cio/ui/base/badge';
const benefitDetails = [
{
icon: 'ai.svg',
title: 'AI-Powered Learning',
checkMark: 'check.svg',
points: [
'Create engaging courses with AI-assisted content generation.',
'Automate grading and feedback processes, saving time and effort.',
'Personalize learning experiences based on individual student needs.'
]
},
{
icon: 'community.svg',
title: 'Foster a Thriving Community',
checkMark: 'check.svg',
points: [
'Create a supportive learning environment with built-in forums and interactive tools.',
'Encourage collaboration and knowledge sharing among students.',
'Build a strong community of learners and mentors.'
]
},
{
icon: 'operations.svg',
title: 'Streamline Operations',
checkMark: 'check.svg',
points: [
'Manage your courses and students efficiently with our user-friendly platform.',
'Issue custom certificates to validate student achievements.',
'Provide a seamless learning experience for your students.'
]
}
];
const featureDetails = [
{
icon: 'certificate-i.svg',
img: 'certificate.svg',
title: 'Certificates',
description:
'We provide pre built certificate templates anyone can choose from and provide to their students right within our dashboard'
},
{
icon: 'forum-i.svg',
img: 'forum.svg',
title: 'Forum area for question and answer',
description:
'Our community section allows for a robust question and answer experience encouraging collaboration between students. This also encourages knowledge sharing amongst students.'
},
{
icon: 'ai.svg',
img: 'ai-pic.svg',
title: 'AI Powered Content creation',
description:
'We give you the ability to create courses with AI, upload videos, embed slides, create quizzes and even grade quizzes with AI.'
},
{
icon: 'free-tools-i.svg',
img: 'free-tools.svg',
title: 'Free tools for students',
description:
'We provide a suite of free tools like Pomodoro timer, Random name picker, and a Progress report generator to make the lives of educators and students much easier.',
ctaLabel: 'Try it out',
ctaLink: '/tools'
}
];
</script>
<svelte:head>
<title>Bootcamps | ClassroomIO</title>
</svelte:head>
<section class="flex flex-col items-center justify-center">
<PageHeader
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
>
<Badge class="bg-white px-4! py-2!" variant="outline">Solution for Bootcamps</Badge>
<div class="flex w-full flex-col items-start justify-start gap-y-8 lg:items-center lg:justify-center">
<div class="flex w-full max-w-7xl flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<h1 class="m-0 mx-auto text-4xl text-slate-900 md:text-6xl lg:text-center">
The platform for fast
<span class="relative text-blue-700"> growing bootcamps</span>
</h1>
<p class="text-base font-normal text-slate-700 md:text-lg lg:w-[60%] lg:text-center">
From AI powered course creation to student collaboration tools, ClassroomIO provides everything you need to
run a cutting-edge, high-impact bootcamp.
</p>
</div>
<Button href="/signup" target="_blank" rel="noopener noreferrer nofollow">Get Started for Free 🚀</Button>
</div>
</PageHeader>
<section
class="flex w-full flex-col justify-center gap-y-12 px-6 py-16 md:px-16 lg:items-center lg:justify-between lg:px-28 lg:py-24"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<Badge class="bg-white px-4! py-2!" variant="outline">Our benefits</Badge>
<div
class="flex max-w-7xl flex-col items-start justify-start gap-y-4 lg:w-full lg:items-center lg:justify-center"
>
<h1 class="text-3xl">Inspire Student Success</h1>
<p class="text-sm font-light text-[#334155] md:w-[80%] md:text-base lg:text-center">
Scale your bootcamp with ClassroomIO's comprehensive toolkit. Create engaging content, foster community, and
drive student achievement.
</p>
</div>
</div>
<div
class="flex w-full max-w-7xl flex-col items-start justify-start gap-6 lg:flex-row lg:items-center lg:justify-between"
>
{#each benefitDetails as benefits}
<div class="flex w-full flex-col gap-y-6 rounded-2xl border border-[#D9E0F5] p-4 md:p-6">
<img
class="h-[48px] w-[48px]"
src="https://assets.cdn.clsrio.com/www/bootcamps/{benefits.icon}"
alt="brain"
/>
<div class="align-start flex w-full flex-col justify-start gap-y-2">
<h1 class="text-md">{benefits.title}</h1>
{#each benefits.points as point}
<div class="flex flex-row items-start justify-start gap-x-3">
<img src="https://assets.cdn.clsrio.com/www/bootcamps/{benefits.checkMark}" alt="check mark" />
<p class="font-[#656565] leading-8 font-light text-gray-500">
{point}
</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
</section>
<section
class="flex w-full flex-col items-center justify-center gap-y-16 bg-[#F1F6FF] px-8 py-16 md:px-16 lg:px-28 lg:py-24"
>
<div class="flex w-full max-w-7xl flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<Badge class="bg-white px-4! py-2!" variant="outline">Explore our features</Badge>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<h1 class="text-3xl">Why We're the Perfect Fit</h1>
<p class="text-sm font-light text-[#334155] md:w-[80%] md:text-base lg:text-center">
Discover why we’re the ideal partner for your bootcamp training needs. Experience innovative solutions
designed to elevate student engagement and maximize learning outcomes.
</p>
</div>
</div>
<div class="flex w-full max-w-7xl flex-col items-center justify-start gap-y-16 lg:gap-y-24">
{#each featureDetails as feature, i}
<div
class="flex w-full lg:w-[70%] lg:flex-row {i % 2 !== 0
? 'lg:flex-row-reverse'
: ''} flex-col items-start justify-between gap-10 lg:items-center lg:justify-between"
>
<div class="flex w-full max-w-sm flex-col items-start justify-start gap-y-3 lg:w-[40%] lg:gap-y-6">
<img
class="h-[48px] w-[48px]"
src="https://assets.cdn.clsrio.com/www/bootcamps/{feature.icon}"
alt="team icon"
/>
<div class="flex flex-col items-start justify-start gap-y-4 md:w-full">
<h1 class="text-2xl">{feature.title}</h1>
<p class="text-lg leading-8 text-gray-500">
{feature.description}
</p>
</div>
{#if feature.ctaLink}
<Button variant="outline" href={feature.ctaLink} target="_blank noopener noreferrer">
{feature.ctaLabel}
</Button>
{:else}
<Button variant="outline" data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">
Book a Demo
</Button>
{/if}
</div>
<div>
<img
class="w-full lg:max-w-[380px]"
src="https://assets.cdn.clsrio.com/www/bootcamps/{feature.img}"
alt="team management feature ui"
/>
</div>
</div>
{/each}
</div>
</section>
<PageSignupCTA
header="Why spend time and money on building your own platform?"
subText="Invest that time and money back into giving your students the best experience possible."
btnLabel="Sign me up"
link="/signup"
demo={false}
/>
</section>
@@ -0,0 +1,348 @@
<script>
import {
AiTutorSection,
CertificateMock,
FeatureGrid,
HeroSubtitle,
ImageDemoSection,
PageHeader,
PageSignupCTA,
SplitFeatureSection,
TrustBand,
WorkflowSteps
} from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import { Meteors } from '@cio/ui/custom/animation/meteors';
import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
import BadgeCheck from '@lucide/svelte/icons/badge-check';
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
import ClipboardCheck from '@lucide/svelte/icons/clipboard-check';
import FileSpreadsheet from '@lucide/svelte/icons/file-spreadsheet';
import FileText from '@lucide/svelte/icons/file-text';
import GraduationCap from '@lucide/svelte/icons/graduation-cap';
import KeyRound from '@lucide/svelte/icons/key-round';
import RefreshCw from '@lucide/svelte/icons/refresh-cw';
import ShieldCheck from '@lucide/svelte/icons/shield-check';
import Users from '@lucide/svelte/icons/users';
const workflow = [
{
title: 'Start with the policies you already have',
description:
'Bring SOPs, PDFs, Notion pages, Google Docs, videos, and internal process notes into one structured course.'
},
{
title: 'Assign training by audience',
description:
'Import people via CSV, group by team or role, and assign the right course to the right audience in bulk.'
},
{
title: 'Set the compliance rules',
description: 'Define passing scores, deadlines, retake intervals, grace periods, reminders, and renewal windows.'
},
{
title: 'Keep proof in one place',
description:
'Track who is compliant, expiring, in progress, waived, or non-compliant without ever opening a spreadsheet.'
}
];
const features = [
{
Icon: ShieldCheck,
title: 'Compliance status tracking',
description:
'Know at a glance whether each learner is compliant, expiring soon, or non-compliant — and surface the people who need a reminder.'
},
{
Icon: RefreshCw,
title: 'Renewals & retake intervals',
description:
'Configure recurring training rules — annual, biannual, custom — so certifications refresh on schedule.'
},
{
Icon: CalendarClock,
title: 'Deadlines & grace periods',
description:
'Set required completion dates and grace windows. Reminders go out automatically before learners fall out of compliance.'
},
{
Icon: ClipboardCheck,
title: 'Waivers, resets, and extensions',
description:
'Handle real-world exceptions without losing the audit history of who was required to complete training.'
},
{
Icon: BadgeCheck,
title: 'Certificates with custom IDs',
description:
'Issue branded PDF/PNG certificates with configurable ID formats (year, month, sequence, org prefix) tied to course completion.'
},
{
Icon: FileSpreadsheet,
title: 'Completion records & certificate ledger',
description:
'Every completion, mark, and issued certificate is stored against the learner — searchable from the dashboard when you need to look up who finished what.'
},
{
Icon: KeyRound,
title: 'SSO and domain enforcement',
description:
'Force SSO by email domain with Better Auth. Break-glass overrides for admins; full sign-in audit history.'
},
{
Icon: Users,
title: 'Audience targeting',
description:
'Bulk enrollment, role-based access, and group assignment. Onboard contractors, vendors, and full-time staff differently.'
}
];
const questionTypes = [
'Multiple choice',
'Checkbox',
'True / false',
'Short answer',
'Paragraph',
'Numeric',
'Fill in the blank',
'Matching',
'Ordering',
'File upload',
'Hotspot',
'Word bank',
'Star rating',
'Links',
'Video recording'
];
const useCases = [
'Annual security awareness',
'GDPR & privacy training',
'HIPAA refreshers',
'AML / KYC training',
'Workplace conduct & harassment',
'Contractor & vendor onboarding',
'Healthcare SOPs',
'Financial-services policy acks',
'Manufacturing safety',
'Role-based access training'
];
</script>
<svelte:head>
<title>Compliance Training | ClassroomIO</title>
<meta
name="description"
content="Turn policies and SOPs into certified training. Track compliance, renewals, grace periods, and audit evidence in one place."
/>
<script
type="module"
src="https://assets.cdn.clsrio.com/embeds/question-type-picker/question-type-picker.js"
></script>
</svelte:head>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto flex max-w-[820px] flex-col items-center text-center">
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<span class="relative flex h-2 w-2">
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-500 opacity-75"></span>
<span class="relative inline-flex h-2 w-2 rounded-full bg-blue-600"></span>
</span>
Compliance Training
</Badge>
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Audit-ready training,
<em class="text-blue-700 not-italic">without the spreadsheets.</em>
</h1>
<HeroSubtitle class="mx-auto">
Turn policies, SOPs, docs, and videos into assigned courses with quizzes, certificates, renewal rules, and
completion evidence. Know exactly who is compliant — and prove it.
</HeroSubtitle>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
<Button href="/signup" variant="outline">Start free</Button>
</div>
<div class="mt-7 flex flex-wrap items-center justify-center gap-5 text-xs text-gray-500">
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> SOC 2-friendly</span>
<span class="flex items-center gap-1.5"
><span class="font-medium text-blue-700">✓</span> Self-hosted option</span
>
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> Open source</span>
</div>
</div>
<div class="mx-auto mt-14 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/org-compliance.jpeg"
alt="Compliance dashboard — required learners grouped by compliant, expiring soon, and non-compliant"
class="block h-auto w-full"
loading="eager"
decoding="async"
/>
</div>
</div>
</PageHeader>
<TrustBand
caption="Built for evidence-based programs"
items={[
'SOC 2 readiness',
'GDPR awareness',
'HIPAA refreshers',
'ISO 27001 controls',
'PCI-DSS staff training',
'AML / KYC'
]}
/>
<WorkflowSteps eyebrow="How it works" title="From policy document to provable completion." steps={workflow} />
<ImageDemoSection
eyebrow="Inside a compliance program"
title="A look inside a compliance program."
description="From importing your team to issuing a certificate — the whole flow at a glance."
imageSrc="/compliance-dashboard.jpeg"
imageAlt="Compliance dashboard — required learners, status, and certificates"
/>
<FeatureGrid
eyebrow="Built for audit pressure"
title="Everything you need when training completion is not optional."
description={'Not a generic LMS with a "compliance" checkbox. Real workflows for the people who have to answer the auditor\u2019s questions.'}
ctaLabel="Get started"
ctaHref="/signup"
{features}
/>
<!-- Renewal split feature -->
<SplitFeatureSection
bgClass="bg-gray-50"
eyebrow="Renewals on autopilot"
title="Training that re-runs itself."
description="Set a retake interval once. ClassroomIO automatically moves learners back into “expiring soon”, sends reminders, holds them in a grace period, and flags non-compliance — all without admin intervention."
imageSrc="/course-compliance.jpeg"
imageAlt="Compliance settings panel — retake interval, grace period, reminders, waiver rules"
>
{#snippet bullets()}
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Annual, biannual, or custom intervals per course
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Reminder cadences before expiry
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Configurable grace periods and waiver workflows
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Pause renewals for leave, role change, or offboarding
</li>
{/snippet}
</SplitFeatureSection>
<!-- Question types showcase -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Assessments</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
15 question types. Twelve auto-graded.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Don't settle for multiple choice. Test understanding the way the policy actually requires — with partial
credit, file uploads, video evidence, and manual override when you need a human in the loop.
</p>
<div class="mt-6 flex flex-wrap gap-2">
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">Auto-graded · 12</span>
<span class="rounded-full bg-amber-50 px-3 py-1 text-xs font-medium text-amber-700">Partial credit · 9</span>
<span class="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700">Manual review · 3</span>
</div>
</div>
<div class="flex justify-center">
<div data-cio-widget="question-type-picker" class="w-full max-w-[900px]"></div>
</div>
</div>
</section>
<!-- Certificates split -->
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div class="order-2 lg:order-1">
<CertificateMock
recipient="Sarah Chen"
achievement="Annual Security Training 2026"
issued="11 May 2026"
verifyValue="CIO-2026-05-0142"
/>
</div>
<div class="order-1 lg:order-2">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Certificates & evidence</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Proof of training, designed for your brand.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Configure the certificate ID format the way your records team needs it — by year, month, sequence, or a custom
prefix. Download as PDF or PNG. Every issuance is logged for audit.
</p>
<ul class="mt-6 space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-2">
<GraduationCap size={16} class="mt-0.5 text-blue-700" /> Templated and branded with your logo and colours
</li>
<li class="flex items-start gap-2">
<BadgeCheck size={16} class="mt-0.5 text-blue-700" /> Tied to a required passing score and assessment
</li>
<li class="flex items-start gap-2">
<AlertTriangle size={16} class="mt-0.5 text-blue-700" /> Auto-revokes when a renewal lapses
</li>
</ul>
</div>
</div>
</section>
<AiTutorSection
accent="In your dashboard"
title="A personal course-authoring assistant."
description="Drop in a policy doc or paste a URL. The assistant plans the compliance course, drafts each lesson, writes the quiz questions, and stops for your approval before publishing — so building required training drops from a week to an afternoon."
imageSrc="/ai-course-builder.jpeg"
imageAlt="Course builder — plan, approve, implement. Templates, in-editor chat, and MCP."
/>
<!-- Use cases -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Use cases</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Training for the work people are required to know.
</h2>
</div>
<div class="overflow-hidden border border-gray-200 bg-white">
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 [&>*:nth-child(2n)]:sm:border-r-0 [&>*:nth-child(2n)]:lg:border-r [&>*:nth-child(3n)]:lg:border-r-0 [&>*:nth-last-child(-n+2)]:sm:border-b-0 [&>*:nth-last-child(-n+3)]:lg:border-b-0"
>
{#each useCases as useCase, i}
<div
class="group relative flex items-center gap-3 border-b border-gray-200 p-6 transition-colors hover:bg-gray-50/60 sm:border-r"
>
<span class="font-mono text-[11px] tracking-[0.15em] text-gray-400">
{String(i + 1).padStart(2, '0')}
</span>
<span class="text-sm font-medium text-gray-900">{useCase}</span>
</div>
{/each}
</div>
</div>
</div>
</section>
<PageSignupCTA
header="Stop chasing compliance manually."
subText="Build certified training from your existing policies, assign it to the right people, and keep proof of completion in one place."
btnLabel="Book a Demo"
/>
</section>
@@ -1,176 +1,392 @@
<script>
import { PageSignupCTA, PageHeader } from '$lib/components';
import { Button } from '@cio/ui/base/button';
import {
AiTutorSection,
BoxedGrid,
CertificateMock,
EmbedCodePreview,
FeatureGrid,
HeroSubtitle,
ImageDemoSection,
LanguageReel,
MultiAcademySection,
PageHeader,
PageSignupCTA,
RoiBand,
TrustBand,
VsHelpDocs,
WorkflowSteps
} from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import { Meteors } from '@cio/ui/custom/animation/meteors';
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
import BookMarked from '@lucide/svelte/icons/book-marked';
import Bot from '@lucide/svelte/icons/bot';
import Code2 from '@lucide/svelte/icons/code-2';
import Crown from '@lucide/svelte/icons/crown';
import GraduationCap from '@lucide/svelte/icons/graduation-cap';
import Languages from '@lucide/svelte/icons/languages';
import MessagesSquare from '@lucide/svelte/icons/messages-square';
import MonitorPlay from '@lucide/svelte/icons/monitor-play';
import PanelTop from '@lucide/svelte/icons/panel-top';
const benefitDetails = [
const workflow = [
{
icon: 'personalised.svg',
title: 'Personalized Learning',
description: 'Offer branded landing pages and multilingual courses for diverse customers.'
title: 'Bring your product knowledge',
description:
'Use help docs, onboarding guides, release notes, demo videos, Looms, and internal customer success notes.'
},
{
icon: 'structured.svg',
title: 'Structured Education',
description: 'Create engaging courses with AI-assisted content generation.'
title: 'Build a customer academy',
description:
'Create structured courses for onboarding, feature adoption, admin training, and power-user education.'
},
{
icon: 'simplified.svg',
title: 'Simplified Management',
description: 'Centralize and easily manage, share, and distribute all customer education materials.'
title: 'Publish it under your brand',
description:
'Custom domain, themed landing pages, public course catalog, and embeddable widgets — your academy, not ours.'
},
{
title: 'Track adoption & retention',
description: 'See progress, assessment results, certificates, and completion data across customers and cohorts.'
}
];
const featureDetails = [
const features = [
{
icon: 'landing-i.svg',
img: 'landing.svg',
title: 'Dedicated landing page',
Icon: PanelTop,
title: 'Branded learning portal',
description:
'We give you a customisable landing page that can be shared with your customers who are interested in learning about your products'
'Customer-facing catalog with landing pages, themes, your logo, your domain, and SEO-ready public pages.'
},
{
icon: 'link.svg',
img: 'domain.svg',
title: 'Custom domains',
description: 'Use your own domain to link to our landing page instead of using our free subdomain'
Icon: MonitorPlay,
title: 'Video & rich media lessons',
description:
'Embed demos, walkthroughs, Looms, MP4s, PDFs, and slide decks. Auto-thumbnails and transcoding included.'
},
{
icon: 'globe.svg',
img: 'language.svg',
title: 'Multi language support',
Icon: BookMarked,
title: 'Programs & learning paths',
description:
'We support 6 languages and you can deploy courses in different languages to support different demographics'
'Bundle courses into onboarding journeys with required completion order, prerequisites, and certificates.'
},
{
icon: 'paths.svg',
img: 'pathways.svg',
title: 'Pathways',
Icon: Bot,
title: 'AI lesson assistant',
description:
'With the introduction of pathways, you can group multiple courses into one comprehensive learning pathway'
'Each learner gets a Claude-powered tutor inside the lesson that can answer questions and summarise content.'
},
{
Icon: MessagesSquare,
title: 'Community & Q&A',
description: 'Per-course discussion forums and program newsfeeds with reactions, threads, and mentions.'
},
{
Icon: GraduationCap,
title: 'Certificates customers can share',
description:
'Issue branded PDF/PNG certificates with verifiable IDs. Great for customer champions and admin programs.'
},
{
Icon: Code2,
title: 'Embeddable widgets',
description:
'Drop courses into your help center, marketing site, or in-product onboarding via a single script tag.'
},
{
Icon: BarChart3,
title: 'Adoption analytics',
description: 'See who completed onboarding, who passed assessments, and which features need more education.'
},
{
Icon: Languages,
title: 'Deliver lessons in 10 languages',
description:
'Per-language lesson variants for English, Spanish, Portuguese, German, French, Hindi, Vietnamese, Russian, Polish, and Danish.'
}
];
const langs = [
{
code: 'en',
native: 'English',
en: 'English',
sample: 'Getting Started: Your First Workspace',
tone: 'from-blue-100 to-blue-200',
ring: 'ring-blue-200'
},
{
code: 'es',
native: 'Español',
en: 'Spanish',
sample: 'Primeros pasos: Tu primer espacio',
tone: 'from-amber-100 to-rose-200',
ring: 'ring-amber-200'
},
{
code: 'pt',
native: 'Português',
en: 'Portuguese',
sample: 'Primeiros passos: Seu primeiro workspace',
tone: 'from-emerald-100 to-teal-200',
ring: 'ring-emerald-200'
},
{
code: 'de',
native: 'Deutsch',
en: 'German',
sample: 'Erste Schritte: Dein erster Arbeitsbereich',
tone: 'from-slate-100 to-slate-200',
ring: 'ring-slate-200'
},
{
code: 'fr',
native: 'Français',
en: 'French',
sample: 'Premiers pas : Votre premier espace',
tone: 'from-blue-100 to-indigo-200',
ring: 'ring-indigo-200'
},
{
code: 'hi',
native: 'हिन्दी',
en: 'Hindi',
sample: 'शुरुआत: आपका पहला वर्कस्पेस',
tone: 'from-orange-100 to-rose-200',
ring: 'ring-orange-200'
},
{
code: 'vi',
native: 'Tiếng Việt',
en: 'Vietnamese',
sample: 'Bắt đầu: Không gian làm việc đầu tiên',
tone: 'from-red-100 to-yellow-200',
ring: 'ring-red-200'
},
{
code: 'ru',
native: 'Русский',
en: 'Russian',
sample: 'Начало работы: ваше рабочее пространство',
tone: 'from-sky-100 to-blue-200',
ring: 'ring-sky-200'
},
{
code: 'pl',
native: 'Polski',
en: 'Polish',
sample: 'Pierwsze kroki: Twoja przestrzeń',
tone: 'from-rose-100 to-red-200',
ring: 'ring-rose-200'
},
{
code: 'da',
native: 'Dansk',
en: 'Danish',
sample: 'Kom godt i gang: Dit første område',
tone: 'from-red-100 to-rose-200',
ring: 'ring-red-200'
}
];
const outcomes = [
{ title: 'Reduce repeat support tickets', sub: 'Self-serve education replaces 1:1 onboarding calls.' },
{ title: 'Shorten time-to-value', sub: 'Customers learn core workflows before their first review call.' },
{ title: 'Train admins and power users', sub: 'Multi-tier programs for end-users vs. champions.' },
{ title: 'Certify customer champions', sub: 'Public shareable certificates fuel community advocacy.' },
{ title: 'Drive feature adoption', sub: 'Targeted micro-courses when you ship something new.' },
{ title: 'Build a moat of educated users', sub: 'The customers who know your product never churn quietly.' }
];
</script>
<svelte:head>
<title>Customer Education | ClassroomIO</title>
<title>Customer academy software for SaaS | ClassroomIO</title>
<meta
name="description"
content="Launch a branded customer academy. Onboarding, feature adoption, admin training, and certification — under your own domain, with an AI tutor in every lesson."
/>
</svelte:head>
<section class="flex flex-col items-center justify-center">
<PageHeader
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
>
<Badge class="bg-white px-4! py-2!" variant="outline">Solution for Customer Education</Badge>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto flex max-w-[820px] flex-col items-center text-center">
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<Crown size={14} class="text-blue-700" />
Customer Education
</Badge>
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Launch a customer academy
<em class="text-blue-700 not-italic">customers actually love.</em>
</h1>
<HeroSubtitle class="mx-auto">
Turn your help docs, onboarding videos, and product knowledge into a branded learning portal. Train customers,
certify power-users, and drive adoption — under your own domain, with an AI tutor in every lesson.
</HeroSubtitle>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
<Button href="/signup" variant="outline">Start free</Button>
</div>
<div class="mt-7 flex flex-wrap items-center justify-center gap-5 text-xs text-gray-500">
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> Custom domain</span>
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> Embed anywhere</span>
<span class="flex items-center gap-1.5"
><span class="font-medium text-blue-700">✓</span> AI in every lesson</span
>
</div>
</div>
<div class="flex w-full flex-col items-start justify-start gap-y-8 lg:items-center lg:justify-center">
<div class="flex w-full max-w-7xl flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<h1 class="m-0 mx-auto w-[95%] text-4xl text-slate-900 md:text-6xl lg:text-center">
Drive retention and revenue with
<span class="relative text-blue-700">Customer Education</span>
</h1>
<p class="text-base font-normal text-slate-700 md:text-lg lg:w-[60%] lg:text-center">
Yes you are making lots of sales however you keep experiencing churn and customers leaving you. Without a
proper customer education strategy you are leaving a lot of money on the table.
</p>
<div class="mx-auto mt-14 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/beautiful-academy-site.jpeg"
alt="Branded customer academy on a custom domain — hero, catalog, themed branding"
class="block h-auto w-full"
loading="eager"
decoding="async"
/>
</div>
<Button href="/signup" target="_blank" rel="noopener noreferrer nofollow">Get Started for Free 🚀</Button>
</div>
</PageHeader>
<section
class="flex w-full flex-col justify-center gap-y-12 px-8 py-16 md:px-16 lg:items-center lg:justify-between lg:px-28 lg:py-24"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<Badge class="bg-white px-4! py-1!" variant="outline">Our benefits</Badge>
<div
class="flex max-w-7xl flex-col items-start justify-start gap-y-4 lg:w-full lg:items-center lg:justify-center"
>
<h1 class="text-3xl">The customer education toolkit</h1>
<p class="text-sm font-light text-[#334155] md:w-[80%] md:text-base lg:text-center">
Create beautiful, branded landing pages to showcase your courses and get customers up to speed quickly
<TrustBand
caption="Built for product-led teams"
items={[
'SaaS onboarding',
'Developer tools',
'B2B platforms',
'Admin training',
'Customer champions',
'Open-source projects'
]}
/>
<RoiBand bgClass="bg-white" />
<WorkflowSteps eyebrow="How it works" title="From scattered docs to a polished academy." steps={workflow} />
<ImageDemoSection
eyebrow="Make it yours"
title="Customize how your customer academy looks like."
description="Theme the catalog, swap fonts and colors to match your brand, and build landing pages that feel like the rest of your product — without losing the LMS underneath."
imageSrc="/customizable-academy.jpeg"
imageAlt="Customizable customer academy — catalog, course page, certificate"
/>
<FeatureGrid
eyebrow="Every piece, included"
title="Everything you need to teach customers your product."
description={'Not a "content site" bolted onto your help docs. A real LMS with programs, assessments, certificates, AI tutoring, community, and analytics, wrapped in your brand.'}
ctaLabel="Get started"
ctaHref="/signup"
{features}
/>
<AiTutorSection
accent=""
title="An AI tutor in every lesson."
description="An AI tutor sits inside every lesson — lesson-aware, multilingual, and patient. It answers the questions customers would have sent to support, before they leave the page."
imagePath="/static/ai-tutor-stack.png"
imageCaption="AI tutor in a lesson — lesson-aware chat, follow-ups, translation, summarisation"
vimeoVideoId="1192910311"
/>
<LanguageReel
{langs}
eyebrow="Global customers"
headlineLead="Teach customers"
headlineAccent="in the language they bought in."
description="Author once, publish in ten. Each lesson supports per-language variants — customers pick the language they read in, certificates render in that language, and adoption analytics roll up across regions."
moduleLabel="Lesson 1 · Onboarding"
footerLabel="6 lessons · 18 min"
bgClass="bg-white"
/>
<EmbedCodePreview
eyebrow="Embed anywhere"
title="Bring lessons to your help center, your app, your marketing site."
description="Publish any course as a widget. Drop a script tag into your help center, in-product onboarding, or marketing page. Progress, marks, and certificates sync back to ClassroomIO."
fileName="help-center.html"
codeLines={[
'<span class="text-slate-500">&lt;!-- in your help center --&gt;</span>',
'<span class="text-pink-400">&lt;script</span> <span class="text-sky-300">src</span><span class="text-slate-500">=</span><span class="text-emerald-300">"https://cdn.classroomio.com/widget.js"</span><span class="text-pink-400">&gt;&lt;/script&gt;</span>',
'',
'<span class="text-pink-400">&lt;div</span> <span class="text-sky-300">data-cio-widget</span><span class="text-slate-500">=</span><span class="text-emerald-300">"getting-started"</span>',
' <span class="text-sky-300">data-org</span><span class="text-slate-500">=</span><span class="text-emerald-300">"yourcompany"</span>',
' <span class="text-sky-300">data-theme</span><span class="text-slate-500">=</span><span class="text-emerald-300">"light"</span><span class="text-pink-400">&gt;</span>',
'<span class="text-pink-400">&lt;/div&gt;</span>'
]}
statusBar="connected · org: yourcompany · widget: getting-started"
previewUrl="help.yourcompany.com / getting-started"
previewImageSrc="/embeddable-widget.jpeg"
previewAlt="Lesson widget embedded inside a help-center page"
capabilities={[
{ label: 'One script tag', sub: 'Drop-in install, no SDK required' },
{ label: 'Select from multiple themes', sub: 'Light, dark, and brand-tuned looks bundled in' },
{ label: 'Sync back to CIO', sub: 'Progress, marks, and certs round-trip' }
]}
bgClass="bg-gray-50"
/>
<!-- Programs / certificates split -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Certified champions</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Turn power-users into a public community.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Bundle courses into Programs, gate them with a required exercise, and issue shareable certificates. Customer
champions love showing off — and your product gets a quiet army of advocates.
</p>
<ul class="mt-6 space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-2">
<GraduationCap size={16} class="mt-0.5 text-blue-700" /> Branded PDF/PNG certificates with verifiable IDs
</li>
<li class="flex items-start gap-2">
<BookMarked size={16} class="mt-0.5 text-blue-700" /> Programs with required completion order
</li>
<li class="flex items-start gap-2">
<MessagesSquare size={16} class="mt-0.5 text-blue-700" /> Per-course Q&A and program-wide newsfeed
</li>
</ul>
</div>
</div>
<div
class="flex w-full max-w-7xl flex-col items-start justify-start gap-6 lg:flex-row lg:items-center lg:justify-between"
>
{#each benefitDetails as benefits}
<div class="flex w-full flex-col gap-y-6 rounded-2xl border border-[#D9E0F5] p-4 md:p-6">
<img
class="h-[48px] w-[48px]"
src="https://assets.cdn.clsrio.com/www/customer-education/{benefits.icon}"
alt="icon"
/>
<div class="align-start flex w-full flex-col justify-start gap-y-2">
<p class="text-lg">{benefits.title}</p>
<div class="flex flex-row items-start justify-start gap-x-3">
<p class="font-[#656565] leading-8 font-light text-gray-500">
{benefits.description}
</p>
</div>
</div>
</div>
{/each}
<CertificateMock
recipient="Alex Rivera"
achievement="YourCo Admin Bootcamp · 2026"
issued="11 May 2026"
verifyValue="CIO-2026-05-0214"
/>
</div>
</section>
<section
class="flex w-full flex-col items-center justify-center gap-y-16 bg-[#F1F6FF] px-8 py-16 md:px-16 lg:px-28 lg:py-24"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<div class="w-fit rounded-full border-2 border-[#C2D2FF] px-4 py-2 text-center">
<p class="text-center text-sm text-[#4B5563]">Explore our features</p>
<!-- Outcomes -->
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-10 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Why teams ship this</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Educated customers churn less. Period.
</h2>
</div>
<div
class="flex max-w-7xl flex-col items-start justify-start gap-y-4 lg:w-full lg:items-center lg:justify-center"
>
<h1 class="text-3xl">Why Choose Us For Education Needs</h1>
<p class="text-sm font-light text-[#334155] md:w-[80%] md:text-base lg:text-center">
Discover how our innovative solutions can reduce stress, enhance productivity, and achieve your training
goals.
</p>
</div>
</div>
<div class="flex w-full max-w-7xl flex-col items-center justify-start gap-y-16 lg:gap-y-24">
{#each featureDetails as feature, i}
<div
class="flex w-full lg:flex-row {i % 2 !== 0
? 'lg:flex-row-reverse'
: ''} flex-col items-start justify-start gap-10 lg:items-center lg:justify-evenly"
>
<div class="flex w-full max-w-sm flex-col items-start justify-start gap-y-3 lg:w-[40%] lg:gap-y-6">
<img src="https://assets.cdn.clsrio.com/www/customer-education/{feature.icon}" alt="team icon" />
<div class="flex flex-col items-start justify-start gap-y-4 md:w-full">
<h1 class="text-2xl">{feature.title}</h1>
<p class="text-lg leading-8 text-gray-500">
{feature.description}
</p>
</div>
<Button variant="outline" data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">
Book a Demo
</Button>
</div>
<div>
<img
class="w-full lg:max-w-[380px]"
src="https://assets.cdn.clsrio.com/www/customer-education/{feature.img}"
alt="team management feature ui"
/>
</div>
</div>
{/each}
<BoxedGrid items={outcomes} cols={3}>
{#snippet cell(outcome)}
<h3 class="text-base font-medium text-gray-950">{outcome.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-gray-500">{outcome.sub}</p>
{/snippet}
</BoxedGrid>
</div>
</section>
<VsHelpDocs bgClass="bg-white" />
<MultiAcademySection />
<PageSignupCTA
header="Stop losing customers to your competitors"
subText="We help you retain customers by helping them understand your products and services."
btnLabel="Book a demo"
demo={true}
header="Launch your customer academy."
subText="Build a branded learning portal customers actually use. Custom domain, AI tutoring, certificates, and analytics — out of the box."
btnLabel="Book a Demo"
/>
</section>
@@ -0,0 +1,141 @@
<script lang="ts">
import { HeroSubtitle, PageHeader, PageSignupCTA, VsHelpDocs } from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import FileText from '@lucide/svelte/icons/file-text';
import GraduationCap from '@lucide/svelte/icons/graduation-cap';
const competitors = [
{
name: 'Intercom Articles',
good: 'Best-in-class in-app help widget. Great search. Where short-form, "right-now" answers belong.',
missing: 'No sequenced learning paths. No assessments. No certification. No per-learner progress.'
},
{
name: 'Zendesk Guide',
good: 'Mature knowledge base, deeply integrated with Zendesk tickets. Excellent for tier-1 deflection.',
missing: 'No course structure, no grading, no certificates. Built for support, not for learning.'
},
{
name: 'Confluence',
good: 'The default internal docs tool. Cheap, ubiquitous, easy to write into.',
missing: 'Not customer-facing by default. No course primitives. No public certs. No learner analytics.'
}
];
const verdicts = [
{
Icon: FileText,
title: 'Keep your help docs.',
body: "When a customer already knows what they need, they search. Help docs win at lookup. Don't rip out the tool that already does that job well."
},
{
Icon: GraduationCap,
title: 'Add a customer academy alongside.',
body: "When a customer needs to learn the product — onboarding, role-based programs, certification, adoption courses for new features — that's the job an academy was built for."
}
];
</script>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto max-w-[1100px]">
<BlurFade delay={0} once>
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">Customer academy vs help docs</Badge>
</BlurFade>
<BlurFade delay={0.1} once>
<h1
class="max-w-[820px] text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950"
>
Why your help center
<em class="text-blue-700 not-italic">isn't teaching</em>
your customers.
</h1>
</BlurFade>
<BlurFade delay={0.2} once>
<HeroSubtitle>
Intercom Articles, Zendesk Guide, and Confluence are excellent at one thing — answering "how do I do X?" when
the user already knows what they need. They were never designed to take a brand-new customer from zero to
confident product use. That's what a customer academy is for.
</HeroSubtitle>
</BlurFade>
<BlurFade delay={0.3} once>
<div class="mt-8 flex flex-wrap items-center gap-3">
<Button href="/signup">Launch your academy free</Button>
<Button href="/customer-education" variant="outline">See a live academy</Button>
</div>
</BlurFade>
</div>
</PageHeader>
<!-- Quick verdict cards -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-8 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">The short answer</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
You need both. They do different jobs.
</h2>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
{#each verdicts as v, i}
<BlurFade delay={0.1 * i} once>
<div class="h-full rounded-2xl border border-gray-200 bg-gray-50 p-7">
<div
class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-white text-blue-700 ring-1 ring-gray-200"
>
<v.Icon size={20} strokeWidth={1.75} />
</div>
<h3 class="text-base font-medium text-gray-950">{v.title}</h3>
<p class="mt-3 text-sm leading-relaxed text-gray-600">{v.body}</p>
</div>
</BlurFade>
{/each}
</div>
</div>
</section>
<!-- The detailed comparison -->
<VsHelpDocs bgClass="bg-gray-50" showHeading={true} />
<!-- Compared head-to-head -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-8 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Tool-by-tool</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
What the popular help-doc tools are great at — and what they aren't.
</h2>
</div>
<div class="grid grid-cols-1 gap-5 md:grid-cols-3">
{#each competitors as competitor, i}
<BlurFade delay={0.08 * i} once>
<div class="flex h-full flex-col rounded-2xl border border-gray-200 bg-white p-6">
<p class="text-base font-medium text-gray-950">{competitor.name}</p>
<div class="mt-5">
<p class="text-[10px] tracking-wider text-emerald-600 uppercase">Where it wins</p>
<p class="mt-1.5 text-sm leading-relaxed text-gray-600">{competitor.good}</p>
</div>
<div class="mt-5">
<p class="text-[10px] tracking-wider text-rose-600 uppercase">Where it doesn't</p>
<p class="mt-1.5 text-sm leading-relaxed text-gray-600">{competitor.missing}</p>
</div>
</div>
</BlurFade>
{/each}
</div>
</div>
</section>
<PageSignupCTA
header="Stop forwarding help articles to every new customer."
subText="Bundle the articles into a real onboarding course, add a final assessment, issue a certificate, and watch retention move."
btnLabel="Book a Demo"
/>
</section>
@@ -0,0 +1,9 @@
export const prerender = true;
export const load = () => ({
pageMetaTags: {
title: 'Customer academy vs help docs (Intercom, Zendesk, Confluence) | ClassroomIO',
description:
'Why a customer academy outperforms help docs for onboarding, certification, and adoption — compared head-to-head with Intercom Articles, Zendesk Guide, and Confluence.'
}
});
@@ -0,0 +1,394 @@
<script lang="ts">
import { PageHeader, PageSignupCTA, WorkflowSteps } from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import { BlurFade } from '@cio/ui/custom/animation/blurfade';
import AlertCircle from '@lucide/svelte/icons/alert-circle';
import BarChart3 from '@lucide/svelte/icons/bar-chart-3';
import BookOpen from '@lucide/svelte/icons/book-open';
import Check from '@lucide/svelte/icons/check';
import CheckCircle2 from '@lucide/svelte/icons/check-circle-2';
import ClipboardCheck from '@lucide/svelte/icons/clipboard-check';
import FileText from '@lucide/svelte/icons/file-text';
import GraduationCap from '@lucide/svelte/icons/graduation-cap';
import Layers from '@lucide/svelte/icons/layers';
import Route from '@lucide/svelte/icons/route';
import Search from '@lucide/svelte/icons/search';
import ShieldCheck from '@lucide/svelte/icons/shield-check';
import Sparkles from '@lucide/svelte/icons/sparkles';
import Users from '@lucide/svelte/icons/users';
const quickVerdicts = [
{
Icon: FileText,
title: 'Use documentation when the reader knows what they need.',
description:
'Docs are best for lookup, reference, troubleshooting, API details, product behavior, release notes, and anything a user needs to search in the moment.'
},
{
Icon: GraduationCap,
title: 'Use a company academy when the learner needs a guided path.',
description:
'Academies are best when people need onboarding, role-based training, practice, proof of completion, certification, or a repeatable learning journey.'
}
];
const comparisonRows = [
{
need: '"How do I do X?"',
docs: true,
academy: false,
note: 'The person already has a task in mind and needs the fastest reliable answer.'
},
{
need: '"Teach me the product from zero"',
docs: false,
academy: true,
note: 'Beginners need sequencing, context, examples, and a clear path from first step to confident use.'
},
{
need: '"Prove I am an expert"',
docs: false,
academy: true,
note: 'Expertise needs assessment, completion records, certificates, or some visible proof of readiness.'
},
{
need: '"Onboard a new partner team"',
docs: false,
academy: true,
note: 'Partner onboarding usually needs role-based paths, assignments, progress tracking, and certification.'
},
{
need: '"Look something up fast"',
docs: true,
academy: false,
note: 'Lookup work should stay searchable, skimmable, linkable, and easy to update.'
},
{
need: '"Make sure everyone completed required training"',
docs: false,
academy: true,
note: 'Completion, reminders, renewals, and audit evidence belong in an academy or LMS.'
},
{
need: '"Explain a technical reference"',
docs: true,
academy: false,
note: 'API details, parameters, edge cases, and error codes work best as documentation.'
},
{
need: '"Launch a repeatable customer education program"',
docs: false,
academy: true,
note: 'A program needs structure: lessons, cohorts or audiences, assessments, certificates, and analytics.'
}
];
const docsUseCases = [
'Product setup steps that users need to revisit',
'API references, SDK details, Webhooks, limits, and errors',
'Feature explanations and configuration options',
'Troubleshooting articles and known issue workarounds',
'Release notes, migration notes, and changelogs',
'Short answers that support, success, and sales teams repeatedly send'
];
const academyUseCases = [
'New employee, customer, or partner onboarding',
'Admin training for complex products or internal systems',
'Certification for resellers, agencies, consultants, or power users',
'Compliance training that needs proof, renewal, and audit evidence',
'Feature adoption programs after a major product launch',
'Role-based enablement for sales, support, implementation, or customer success teams'
];
const decisionSignals = [
{
Icon: Search,
title: 'If the audience is searching, write docs.',
description:
'Search intent usually means the person already has context. They need precision, not a course. Make the page easy to scan, link related topics, and keep it current.'
},
{
Icon: Route,
title: 'If the audience needs sequence, build an academy.',
description:
'Sequence matters when concepts depend on each other. A company academy can start with foundations, add practice, check understanding, then issue proof.'
},
{
Icon: ClipboardCheck,
title: 'If the business needs proof, build an academy.',
description:
'Docs can inform, but they rarely prove readiness. Use courses, assessments, completion records, and certificates when managers need to know who is trained.'
},
{
Icon: Users,
title: 'If different roles need different outcomes, build paths.',
description:
'A support agent, admin, reseller, and executive sponsor should not read the same long documentation tree. Academies let each role follow a purpose-built path.'
}
];
const hybridModel = [
{
title: 'Docs stay as the source of truth',
description:
'Keep canonical facts in documentation: product behavior, technical references, setup options, screenshots, limitations, and troubleshooting details.'
},
{
title: 'Academy packages the learning journey',
description:
'Turn the most important docs into guided lessons with context, order, exercises, quizzes, examples, and certificates.'
},
{
title: 'Support and success route people to the right layer',
description:
'Send advanced users to docs for quick answers. Send new users, partners, and internal teams to academy paths when they need structured training.'
}
];
const academySignals = [
'People keep asking for onboarding calls even though docs exist.',
'Customers read the docs but still do not adopt the full workflow.',
'Managers ask who has completed training.',
'Partners need certification before they sell or implement.',
'Compliance, policy, or process training needs renewal evidence.',
'Product changes require more than an announcement.'
];
</script>
<svelte:head>
<title>Docs vs Company Academy | ClassroomIO</title>
<meta
name="description"
content="A practical guide to when your company needs documentation, when it needs a company academy, and how both work together for onboarding, enablement, and certification."
/>
</svelte:head>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto flex max-w-[820px] flex-col items-center text-center">
<BlurFade delay={0} once>
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<BookOpen size={14} class="text-blue-700" />
Docs vs Company Academy
</Badge>
</BlurFade>
<BlurFade delay={0.1} once>
<h1 class="text-[clamp(2.4rem,4vw,3.6rem)] leading-[1.08] font-medium tracking-tight text-gray-950">
Documentation answers questions.
<em class="text-blue-700 not-italic">A company academy teaches them to do the job.</em>
</h1>
</BlurFade>
<BlurFade delay={0.2} once>
<p class="mt-6 text-sm leading-relaxed lg:text-base">
Most teams do not need to choose one forever. They need to understand the job each format performs. This guide
explains when docs are enough, when a company academy is the better tool, and how the two should work
together.
</p>
</BlurFade>
<BlurFade delay={0.3} once>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
<Button href="/customer-education" variant="outline">Explore academies</Button>
</div>
</BlurFade>
</div>
<BlurFade delay={0.35} once>
<div class="mx-auto mt-14 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/docs-vs-customer-site.jpeg"
alt="Docs answering questions, customer academy building capability — side by side"
class="block h-auto w-full"
loading="eager"
decoding="async"
/>
</div>
</div>
</BlurFade>
</PageHeader>
<section class="bg-gray-50 px-6 py-16 lg:px-12 lg:py-24">
<div class="mx-auto max-w-[1120px]">
<div class="mb-10 max-w-[720px]">
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">Decision table</div>
<h2 class="text-[clamp(2rem,3vw,2.7rem)] leading-[1.15] font-medium tracking-tight text-gray-950">
Start with the need, then choose the format.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
If the person wants one answer, documentation is usually right. If the person needs a path, proof, or
repeatable onboarding, a company academy is usually right.
</p>
</div>
<div class="overflow-hidden rounded-2xl border border-gray-200 bg-white">
<div
class="grid grid-cols-[1fr_84px_96px] border-b border-gray-200 bg-gray-950 text-white md:grid-cols-[1fr_110px_130px_1.3fr]"
>
<div class="px-5 py-4 text-sm font-medium">Need</div>
<div class="px-3 py-4 text-center text-sm font-medium md:px-5">Docs</div>
<div class="px-3 py-4 text-center text-sm font-medium md:px-5">Academy</div>
<div class="hidden px-5 py-4 text-sm font-medium md:block">Why</div>
</div>
{#each comparisonRows as row}
<div
class="grid grid-cols-[1fr_84px_96px] border-b border-gray-100 last:border-b-0 md:grid-cols-[1fr_110px_130px_1.3fr]"
>
<div class="bg-gray-50 px-5 py-5 text-sm font-medium text-gray-950">{row.need}</div>
<div class="flex items-center justify-center px-3 py-5 md:px-5">
{#if row.docs}
<span class="text-xl text-green-600" aria-label="Yes">✓</span>
{:else}
<span class="text-xl text-red-500" aria-label="No">×</span>
{/if}
</div>
<div class="flex items-center justify-center px-3 py-5 md:px-5">
{#if row.academy}
<span class="text-xl text-green-600" aria-label="Yes">✓</span>
{:else}
<span class="text-xl text-red-500" aria-label="No">×</span>
{/if}
</div>
<div class="hidden px-5 py-5 text-sm leading-relaxed text-gray-600 md:block">{row.note}</div>
</div>
{/each}
</div>
</div>
</section>
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1120px]">
<div class="grid grid-cols-1 lg:grid-cols-2 lg:divide-x lg:divide-gray-200">
<!-- When docs are enough -->
<div class="lg:pr-10">
<div class="text-[11px] font-medium tracking-widest text-blue-700 uppercase">When docs are enough</div>
<h2 class="mt-4 text-[clamp(1.6rem,2.6vw,2.2rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Choose documentation when the task is searchable, narrow, and repeatable.
</h2>
<p class="mt-5 text-sm leading-relaxed text-gray-500">
Documentation works best when the user has a specific job and needs a precise answer. Easy to find, easy to
scan, easy to trust — no enrollment required.
</p>
<div class="my-7 border-t border-gray-200"></div>
<ul class="space-y-4">
{#each docsUseCases as useCase}
<li class="flex items-start gap-3 text-sm text-gray-700">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-blue-50 text-blue-700"
>
<Check size={12} strokeWidth={2.5} />
</span>
{useCase}
</li>
{/each}
</ul>
<div class="mt-8">
<span
class="inline-flex items-center gap-2 rounded-full bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700"
>
<FileText size={13} strokeWidth={2} />
Reference & retrieval
</span>
</div>
</div>
<!-- When an academy is better -->
<div class="mt-12 lg:mt-0 lg:pl-10">
<div class="text-[11px] font-medium tracking-widest text-emerald-700 uppercase">
When an academy is better
</div>
<h2 class="mt-4 text-[clamp(1.6rem,2.6vw,2.2rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Choose a company academy when capability, behavior change, or proof matters.
</h2>
<p class="mt-5 text-sm leading-relaxed text-gray-500">
An academy turns scattered knowledge into a structured path. Use it when the learner needs context before
details, practice before independence, or a certificate before being trusted with a role.
</p>
<div class="my-7 border-t border-gray-200"></div>
<ul class="space-y-4">
{#each academyUseCases as useCase}
<li class="flex items-start gap-3 text-sm text-gray-700">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-emerald-700"
>
<Check size={12} strokeWidth={2.5} />
</span>
{useCase}
</li>
{/each}
</ul>
<div class="mt-8">
<span
class="inline-flex items-center gap-2 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700"
>
<GraduationCap size={13} strokeWidth={2} />
Structured learning
</span>
</div>
</div>
</div>
</div>
</section>
<WorkflowSteps
eyebrow="Decision framework"
title="Ask these four questions before creating another content surface."
description="The wrong format creates expensive confusion. Long docs cannot force mastery, and long courses are frustrating when someone only needs one answer."
steps={decisionSignals.map((signal, i) => ({
title: signal.title,
description: signal.description,
label: `Question ${String(i + 1).padStart(2, '0')}`
}))}
/>
<section class="px-6 py-16 lg:px-12 lg:py-24">
<div class="mx-auto grid max-w-[1120px] grid-cols-1 items-start gap-12 lg:grid-cols-[0.9fr_1.1fr] lg:gap-16">
<div>
<div class="mb-3 text-xs font-medium tracking-widest text-blue-700 uppercase">The hybrid model</div>
<h2 class="text-[clamp(2rem,3vw,2.7rem)] leading-[1.15] font-medium tracking-tight text-gray-950">
The strongest teams use docs and academies together.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Treat documentation as the reference layer and the academy as the enablement layer. The academy should not
duplicate every page in your docs. It should organize the important material into an outcome-driven path.
</p>
</div>
<div class="space-y-4">
{#each hybridModel as item, i}
<BlurFade delay={0.08 * i} once>
<div class="flex gap-4 rounded-2xl border border-gray-200 bg-white p-5">
<div
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-700 text-sm font-medium text-white"
>
{i + 1}
</div>
<div>
<h3 class="text-base font-medium text-gray-950">{item.title}</h3>
<p class="mt-2 text-sm leading-relaxed text-gray-500">{item.description}</p>
</div>
</div>
</BlurFade>
{/each}
</div>
</div>
</section>
<PageSignupCTA
header="ClassroomIO can host your company academy."
subText="Turn your docs, videos, policies, and product knowledge into a branded academy with courses, assessments, certificates, analytics, and a custom domain."
btnLabel="Book a Demo"
/>
</section>
@@ -1,199 +0,0 @@
<script>
import { PageSignupCTA, PageHeader } from '$lib/components';
import { Button } from '@cio/ui/base/button';
import { Badge } from '@cio/ui/base/badge';
const benefitDetails = [
{
icon: 'light-bulb.svg',
title: 'Grow from within',
description: 'Employees who learn new skills stay longer, perform better, and take on bigger challenges.'
},
{
icon: 'learning.svg',
title: 'Training that sticks',
description: 'Structured courses with quizzes and certifications so your team actually retains what they learn.'
},
{
icon: 'pie-chart.svg',
title: 'See the ROI',
description: 'Track completion rates, skill gaps, and progress across every department in real time.'
},
{
icon: 'paint-brush.svg',
title: 'Make it yours',
description: 'Your logo, your colors, your domain. Employees see your brand, not ours.'
}
];
const featureDetails = [
{
icon: 'team-mgnt.svg',
img: 'team-mgnt2.svg',
title: 'Enroll entire teams in minutes',
description:
'Invite managers and employees by department or role. Assign the right training to the right people without the back and forth.'
},
{
icon: 'colour-pallete.svg',
img: 'custom.svg',
title: 'A learning platform that feels like yours',
description:
'Set your company logo, brand colors, and custom domain. Your employees get a seamless experience that looks and feels internal.'
},
{
icon: 'cup.svg',
img: 'progress.svg',
title: 'Know exactly where your team stands',
description: 'See who completed what, who needs a nudge, and where the skill gaps are. All in one dashboard.'
}
];
</script>
<svelte:head>
<title>Employee Training | ClassroomIO</title>
</svelte:head>
<section class="flex flex-col items-center justify-center">
<PageHeader
className="flex flex-col lg:items-center items-start lg:justify-center justify-start gap-y-6 lg:px-28 md:px-16 px-8"
>
<Badge class="bg-white px-4! py-2!" variant="outline">Invest in your people</Badge>
<div class="flex w-full flex-col items-start justify-start gap-y-8 lg:items-center lg:justify-center">
<div class="flex w-full max-w-7xl flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<h1 class="m-0 mx-auto text-4xl text-slate-900 md:text-6xl lg:text-center">
Your best investment is
<br /><span class="relative text-blue-700">your employees</span>
</h1>
<p class="text-base font-normal text-slate-700 md:text-lg lg:w-[60%] lg:text-center">
Companies that invest in employee growth outperform the ones that don't. Give your team the tools to learn,
grow, and lead.
</p>
</div>
<Button href="/signup" target="_blank">Start investing in your team</Button>
</div>
</PageHeader>
<section
class="flex w-full max-w-7xl flex-col justify-center gap-x-6 gap-y-8 px-8 py-16 md:px-16 lg:flex-row lg:items-center lg:justify-between lg:px-28 lg:py-24"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:w-[35%]">
<Badge class="bg-white" variant="outline">Why it matters</Badge>
<div class="flex flex-col items-start justify-start gap-y-4 md:w-full">
<h1 class="text-3xl">Great teams are built, not hired</h1>
<p class="text-lg leading-8 text-gray-500">
The companies people want to work for are the ones that help them grow. Training is how you get there.
</p>
</div>
</div>
<div
class="flex w-full flex-col items-center justify-start gap-6 lg:w-[60%] lg:min-w-[450px] lg:flex-row lg:flex-wrap"
>
{#each benefitDetails as benefits}
<div
class="flex w-full min-w-[220px] grow flex-col gap-y-6 rounded-2xl border border-[#D9E0F5] p-4 md:p-6 lg:w-[47%]"
>
<img
class="h-[48px] w-[48px]"
src="https://assets.cdn.clsrio.com/www/employee-training/{benefits.icon}"
alt={benefits.title}
/>
<div class="align-start flex w-full flex-col justify-start gap-y-2">
<h1 class="text-lg">{benefits.title}</h1>
<p class="font-[#656565] text-base leading-8 font-light text-gray-500">
{benefits.description}
</p>
</div>
</div>
{/each}
</div>
</section>
<section
class="flex w-full flex-col items-center justify-start gap-y-16 bg-[#F1F6FF] px-8 py-16 md:px-16 lg:px-28 lg:py-24"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:items-center lg:justify-center">
<Badge class="bg-white px-4! py-2!" variant="outline">How it works</Badge>
<div
class="flex max-w-7xl flex-col items-start justify-start gap-y-4 lg:w-full lg:items-center lg:justify-center"
>
<h1 class="text-3xl">Everything you need to grow your team</h1>
<p class="text-sm font-light text-[#334155] md:w-[80%] md:text-base lg:text-center">
From onboarding to upskilling, ClassroomIO gives you the tools to build a culture of continuous learning.
</p>
</div>
</div>
<div class="flex w-full max-w-7xl flex-col items-center justify-start gap-16 p-2 lg:gap-y-24">
{#each featureDetails as feature, i}
<div
class="flex w-full lg:w-[70%] lg:flex-row {i % 2 !== 0
? 'lg:flex-row-reverse'
: ''} flex-col items-start justify-between gap-10 lg:items-center"
>
<div class="flex w-fit max-w-sm flex-col items-start justify-start gap-y-3 lg:w-[40%] lg:gap-y-6">
<img
class="h-[48px] w-[48px]"
src="https://assets.cdn.clsrio.com/www/employee-training/{feature.icon}"
alt={feature.title}
/>
<div class="flex flex-col items-start justify-start gap-y-4 md:w-full">
<h1 class="text-2xl">{feature.title}</h1>
<p class="text-lg leading-8 text-gray-500">
{feature.description}
</p>
</div>
<Button variant="outline" data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">
Book a Demo
</Button>
</div>
<div>
<img
class="w-full lg:max-w-[380px]"
src="https://assets.cdn.clsrio.com/www/employee-training/{feature.img}"
alt={feature.title}
/>
</div>
</div>
{/each}
</div>
</section>
<section
class="flex w-full flex-col items-center justify-start gap-y-16 bg-[#040F2D] px-8 py-16 md:px-16 lg:px-28 lg:py-24"
>
<div
class="flex w-full max-w-7xl flex-col-reverse items-start justify-start gap-y-8 lg:flex-row-reverse lg:items-center lg:justify-between"
>
<div class="flex w-full flex-col items-start justify-start gap-y-4 lg:w-[40%]">
<div class="flex flex-col items-start justify-start gap-y-4 md:w-full">
<h1 class="text-2xl text-white">Need something specific for your team?</h1>
<p class="text-base leading-8 text-[#DEDEDE]">
We work with companies to build custom training solutions. Tell us what your team needs and we'll make it
happen.
</p>
</div>
<Button href="/demo" target="_blank" variant="outline">Talk to sales</Button>
</div>
<img
class="size-96"
src="https://assets.cdn.clsrio.com/www/employee-training/bro.svg"
alt="male and female in an office"
/>
</div>
</section>
<PageSignupCTA
header="The best time to invest in your team is now"
subText="Start for free. No credit card required."
btnLabel="Get started"
link="/signup"
demo={false}
/>
</section>
@@ -0,0 +1,366 @@
<script>
import {
EmbedCodePreview,
FeatureGrid,
HeroSubtitle,
ImageDemoSection,
LanguageReel,
PageHeader,
PageSignupCTA,
TrustBand,
WorkflowSteps
} from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import { Meteors } from '@cio/ui/custom/animation/meteors';
import BadgeCheck from '@lucide/svelte/icons/badge-check';
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
import Boxes from '@lucide/svelte/icons/boxes';
import ChartNoAxesCombined from '@lucide/svelte/icons/chart-no-axes-combined';
import Code2 from '@lucide/svelte/icons/code-2';
import Globe from '@lucide/svelte/icons/globe';
import Languages from '@lucide/svelte/icons/languages';
import Layers from '@lucide/svelte/icons/layers';
import MessageSquareText from '@lucide/svelte/icons/message-square-text';
import Share2 from '@lucide/svelte/icons/share-2';
import Sparkles from '@lucide/svelte/icons/sparkles';
import Trophy from '@lucide/svelte/icons/trophy';
const steps = [
{
title: 'Spin up a partner workspace',
description:
'Add a separate partner workspace under your existing account. Its own brand, its own domain, its own catalog. Customers never see partner content, and partners never see customer content.'
},
{
title: 'Package your product knowledge',
description:
'Bring in your docs, sales playbooks, release notes, demo videos, and enablement Looms. The AI assistant turns them into a structured course you can edit.'
},
{
title: 'Design certification paths',
description:
'Group courses into Programs for onboarding, sales, implementation, and advanced topics, with required exercises and a final exam.'
},
{
title: 'Track readiness and certify',
description: 'See progress, marks, submissions, and issued certificates across partner orgs and individual reps.'
}
];
const features = [
{
Icon: Layers,
title: 'Programs and learning paths',
description: 'Bundle multiple courses into a certification journey with a required order and shared progress.'
},
{
Icon: BadgeCheck,
title: 'Branded partner certificates',
description:
'Issue PDF and PNG certificates with your logo, configurable IDs, and a public verification URL partners can share.'
},
{
Icon: Globe,
title: 'Custom domain and branded portal',
description:
'Run your partner academy on your own domain via Cloudflare. Theme, logo, landing pages, and SEO are all yours.'
},
{
Icon: Share2,
title: 'Public pages and embeds',
description:
'Publish course landing pages, or embed a learning widget on your partner portal, intranet, or marketing site.'
},
{
Icon: Languages,
title: 'Multilingual content',
description:
'Deliver lessons in 10 languages. Author once and publish per-language variants for English, Spanish, Portuguese, German, French, Hindi, and more.'
},
{
Icon: ChartNoAxesCombined,
title: 'Performance analytics',
description:
'Drill into completion, time-to-certify, exercise marks, and submission quality, by partner, cohort, or geography.'
},
{
Icon: MessageSquareText,
title: 'AI lesson assistant',
description:
'Each partner gets a Claude-powered tutor inside the course that can summarise lessons and answer product questions.'
},
{
Icon: Boxes,
title: 'Cohort enrolment and invites',
description:
'Import full partner orgs from CSV, assign by role, gate paid programs, and track who has accepted their invite.'
}
];
const langs = [
{
code: 'en',
native: 'English',
en: 'English',
sample: 'Implementation Checklist',
tone: 'from-blue-100 to-blue-200',
ring: 'ring-blue-200'
},
{
code: 'es',
native: 'Español',
en: 'Spanish',
sample: 'Lista de implementación',
tone: 'from-amber-100 to-rose-200',
ring: 'ring-amber-200'
},
{
code: 'pt',
native: 'Português',
en: 'Portuguese',
sample: 'Lista de implementação',
tone: 'from-emerald-100 to-teal-200',
ring: 'ring-emerald-200'
},
{
code: 'de',
native: 'Deutsch',
en: 'German',
sample: 'Implementierungs-Checkliste',
tone: 'from-slate-100 to-slate-200',
ring: 'ring-slate-200'
},
{
code: 'fr',
native: 'Français',
en: 'French',
sample: 'Liste de mise en œuvre',
tone: 'from-blue-100 to-indigo-200',
ring: 'ring-indigo-200'
},
{
code: 'hi',
native: 'हिन्दी',
en: 'Hindi',
sample: 'कार्यान्वयन चेकलिस्ट',
tone: 'from-orange-100 to-rose-200',
ring: 'ring-orange-200'
},
{
code: 'vi',
native: 'Tiếng Việt',
en: 'Vietnamese',
sample: 'Danh sách triển khai',
tone: 'from-red-100 to-yellow-200',
ring: 'ring-red-200'
},
{
code: 'ru',
native: 'Русский',
en: 'Russian',
sample: 'Контрольный список',
tone: 'from-sky-100 to-blue-200',
ring: 'ring-sky-200'
},
{
code: 'pl',
native: 'Polski',
en: 'Polish',
sample: 'Lista wdrożeniowa',
tone: 'from-rose-100 to-red-200',
ring: 'ring-rose-200'
},
{
code: 'da',
native: 'Dansk',
en: 'Danish',
sample: 'Implementeringstjekliste',
tone: 'from-red-100 to-rose-200',
ring: 'ring-red-200'
}
];
</script>
<svelte:head>
<title>Partner Training | ClassroomIO</title>
<meta
name="description"
content="Run a partner academy alongside your customer one. Separate brand, separate domain, same account. Certify resellers, agencies, and integrators with programs, certificates, and analytics."
/>
</svelte:head>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto flex max-w-[820px] flex-col items-center text-center">
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<Trophy size={14} class="text-blue-700" />
Partner Training
</Badge>
<h1 class="text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Train the partners
<em class="text-blue-700 not-italic">who sell your product.</em>
</h1>
<HeroSubtitle class="mx-auto">
Run a partner workspace alongside your customer academy. Separate brand, separate domain, same account. Certify
resellers, agencies, and integrators with programs and assessments that stay in sync as your product changes.
</HeroSubtitle>
<div class="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button data-cal-config="'layout':'month_view'" data-cal-link="classroomio/demo">Book a Demo</Button>
<Button href="/signup" variant="outline">Start free</Button>
</div>
<div class="mt-7 flex flex-wrap items-center justify-center gap-5 text-xs text-gray-500">
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> Custom domain</span>
<span class="flex items-center gap-1.5"
><span class="font-medium text-blue-700">✓</span> Separate workspace</span
>
<span class="flex items-center gap-1.5"><span class="font-medium text-blue-700">✓</span> Embeddable</span>
</div>
</div>
<div class="mx-auto mt-14 w-full max-w-[1100px]">
<div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-gray-200">
<img
src="/latest-courses.jpeg"
alt="Partner academy — latest courses, program structure, and learner state"
class="block h-auto w-full"
loading="eager"
decoding="async"
/>
</div>
</div>
</PageHeader>
<TrustBand
caption="For partner programs at"
items={[
'SaaS platforms',
'Fintech',
'Developer tools',
'Solution agencies',
'Franchise networks',
'Channel programs'
]}
/>
<WorkflowSteps
eyebrow="Partner enablement"
title="Give every partner the same starting point."
{steps}
bgClass="bg-gray-50"
/>
<ImageDemoSection
eyebrow="Public course pages"
title="Open a few courses to the public, gate the rest."
description="Pick the courses you want anyone to see. Partners (and the curious ones who aren’t partners yet) can jump in without signing up, watch a few lessons, and if they’re hooked they create an account to unlock the rest of your catalog."
imageSrc="/public-courses.jpeg"
imageAlt="Public partner course pages — open courses up front, sign-up gates the rest"
/>
<FeatureGrid
eyebrow="What you can run"
title="Certification programs for the people who represent your product."
{features}
/>
<EmbedCodePreview
eyebrow="Embed anywhere"
title="Drop a learning widget into any site."
description="Generate a published widget payload for any course, paste the snippet into your partner portal, intranet, or marketing page. Progress, marks, and certificates sync back to ClassroomIO automatically."
fileName="partner-portal.html"
codeLines={[
'<span class="text-slate-500">&lt;!-- in your partner portal --&gt;</span>',
'<span class="text-pink-400">&lt;script</span> <span class="text-sky-300">src</span><span class="text-slate-500">=</span><span class="text-emerald-300">"https://cdn.classroomio.com/widget.js"</span><span class="text-pink-400">&gt;&lt;/script&gt;</span>',
'',
'<span class="text-pink-400">&lt;div</span> <span class="text-sky-300">data-cio-widget</span><span class="text-slate-500">=</span><span class="text-emerald-300">"partner-onboarding"</span>',
' <span class="text-sky-300">data-org</span><span class="text-slate-500">=</span><span class="text-emerald-300">"yourcompany"</span>',
' <span class="text-sky-300">data-theme</span><span class="text-slate-500">=</span><span class="text-emerald-300">"light"</span>',
' <span class="text-sky-300">data-lang</span><span class="text-slate-500">=</span><span class="text-emerald-300">"auto"</span><span class="text-pink-400">&gt;</span>',
'<span class="text-pink-400">&lt;/div&gt;</span>'
]}
statusBar="connected · org: yourcompany · widget: partner-onboarding"
previewUrl="partners.yourcompany.com / academy"
previewImageSrc="/embeddable-widget.jpeg"
previewAlt="Course widget embedded inside a partner portal"
capabilities={[
{ label: 'One script tag', sub: 'Drop-in install, no SDK required' },
{ label: 'Select from multiple themes', sub: 'Light, dark, and brand-tuned looks bundled in' },
{ label: 'Sync back to CIO', sub: 'Progress, marks, and certs round-trip' }
]}
/>
<LanguageReel
{langs}
eyebrow="Global partners"
headlineLead="Train your network"
headlineAccent="in their language."
description="Author once, publish in ten. Each lesson supports per-language variants. Learners pick the language they read in, certificates render in that language, and analytics roll up across regions."
moduleLabel="Module 2 · Implementation"
footerLabel="8 lessons"
/>
<!-- Analytics split -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Readiness analytics</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
See which partners are ready to sell, and which need help.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Drill into completion rates, time-to-certify, exercise marks, and submission quality. Filter by partner org,
cohort, geography, or program. Export anything to CSV.
</p>
<ul class="mt-6 space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-2">
<BookOpenCheck size={16} class="mt-0.5 text-blue-700" /> Course-level and program-level dashboards
</li>
<li class="flex items-start gap-2">
<ChartNoAxesCombined size={16} class="mt-0.5 text-blue-700" /> Per-learner gradebook with manual override
</li>
<li class="flex items-start gap-2">
<MessageSquareText size={16} class="mt-0.5 text-blue-700" /> Newsfeed activity and Q&A participation
</li>
</ul>
</div>
<!-- IMAGE STUB: partner-analytics-dashboard.png -->
<div class="relative overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl/5">
<div class="aspect-[4/3] w-full bg-gradient-to-br from-blue-50 via-white to-gray-50 p-6">
<div class="grid h-full grid-cols-2 gap-3">
<div class="rounded-xl bg-white p-3 ring-1 ring-gray-100">
<p class="text-[10px] text-gray-500">Time to certify</p>
<p class="mt-1 text-xl font-medium text-gray-950">11.2d</p>
<p class="text-[10px] text-green-600">↓ 2.4d</p>
</div>
<div class="rounded-xl bg-white p-3 ring-1 ring-gray-100">
<p class="text-[10px] text-gray-500">Pass rate</p>
<p class="mt-1 text-xl font-medium text-blue-700">87%</p>
</div>
<div class="col-span-2 rounded-xl bg-white p-3 ring-1 ring-gray-100">
<p class="mb-2 text-[10px] text-gray-500">Completion by partner org</p>
<div class="space-y-1.5">
{#each [{ n: 'Northwind', v: 92 }, { n: 'Acme Resellers', v: 78 }, { n: 'Globex Implements', v: 64 }] as row}
<div class="flex items-center gap-2">
<span class="w-24 text-[10px] text-gray-600">{row.n}</span>
<div class="h-1.5 flex-1 overflow-hidden rounded-full bg-gray-100">
<div class="h-full rounded-full bg-blue-700" style="width: {row.v}%"></div>
</div>
<span class="text-[10px] font-medium text-gray-700">{row.v}%</span>
</div>
{/each}
</div>
</div>
</div>
</div>
<p class="pb-3 text-center text-xs text-gray-400">Replace with /static/partner-analytics-dashboard.png</p>
</div>
</div>
</section>
<PageSignupCTA
header="Certify your next partner cohort."
subText="Turn your product knowledge into a repeatable program. Courses, certificates, branded portal, and analytics in one place."
btnLabel="Book a Demo"
/>
</section>
+5 -8
View File
@@ -5,7 +5,6 @@
import extend from 'just-extend';
import { onMount } from 'svelte';
import { MetaTags } from 'svelte-meta-tags';
import PageTransition from './transition.svelte';
import '../app.css';
@@ -25,13 +24,11 @@
<div class="overflow-hidden bg-white">
<Navigation stars={data.stars} />
<PageTransition url={data.url}>
{#if page.status === 404}
<NotFound className="mt-5" />
{:else}
{@render children?.()}
{/if}
</PageTransition>
{#if page.status === 404}
<NotFound className="mt-5" />
{:else}
{@render children?.()}
{/if}
<Footer />
</div>
+27 -16
View File
@@ -1,31 +1,42 @@
<script>
import {
Hero,
CertSection,
RoiBand,
AiBuildsSection,
FeaturesSection,
AiSection,
LearnerSection,
QuestionTypes,
AutomateSection,
DeploySection,
EnterpriseSection,
AiTutorSection,
AutomationBand,
MultiAcademySection,
Testimonial,
PageSignupCTA
} from '$lib/components';
</script>
<Hero />
<AiSection />
<RoiBand />
<AiBuildsSection />
<FeaturesSection />
<CertSection />
<LearnerSection />
<QuestionTypes />
<!-- <AutomateSection /> -->
<!-- <DeploySection /> -->
<EnterpriseSection />
<AiTutorSection
accent=""
title="AI-powered Teaching Assistant"
description="An AI teaching assistant that adapts to each student — answering questions in their own words, adjusting to their pace, and catching the ones that would otherwise land in your support inbox."
ctaLabel="See the academy"
ctaHref="/customer-education"
vimeoVideoId="1192910311"
/>
<AutomationBand />
<MultiAcademySection />
<Testimonial />
<PageSignupCTA
header="Ready to replace Moodle with something you'll actually enjoy?"
subText="Get up and running in an afternoon. No vendor calls, no implementation fees, no surprises."
header="Stop forwarding help articles."
subText="Launch a branded academy on your domain in days."
btnLabel="Book a Demo"
/>
@@ -1,65 +0,0 @@
import { PUBLIC_SERVER_URL } from '$env/static/public';
const sendEmail = (sFetch: typeof fetch) => async (subject: string, content: string, replyTo?: string) => {
try {
const response = await sFetch(`${PUBLIC_SERVER_URL}/sendEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify([
{
from: `"ClassroomIO Support" <notify@mail.classroomio.com>`,
to: 'help@classroomio.com',
subject,
content,
replyTo
}
])
});
const responseData = await response.json();
console.log('Emails sent successfully:', responseData);
return 'sent';
} catch (error) {
console.error('Error sending email', error);
return 'failed';
}
};
export async function POST({ request, fetch }) {
try {
const { title, description, replyTo } = await request.json();
console.log(title, description, replyTo);
const result = await sendEmail(fetch)(title, description, replyTo);
return new Response(
JSON.stringify({
message: 'Issue created successfully',
issue: result
}),
{
status: 201,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.log('errors from api', error);
return new Response(
JSON.stringify({
message: 'Failed to create issue',
error: (error as Error)?.message
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
}
@@ -0,0 +1,212 @@
<script lang="ts">
import { CodeBlock, HeroSubtitle, McpServerSection, PageHeader, PageSignupCTA } from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import Code2 from '@lucide/svelte/icons/code-2';
import Github from '@lucide/svelte/icons/github';
const apiCurl = [
'<span class="text-slate-500"># Enroll a learner in a course</span>',
'<span class="text-pink-400">curl</span> <span class="text-emerald-300">"https://api.classroomio.com/v1/enrollments"</span> \\',
' <span class="text-sky-300">-H</span> <span class="text-emerald-300">"Authorization: Bearer $CIO_API_KEY"</span> \\',
' <span class="text-sky-300">-H</span> <span class="text-emerald-300">"Content-Type: application/json"</span> \\',
' <span class="text-sky-300">-d</span> <span class="text-emerald-300">\'{ "courseId": "crs_…", "userId": "usr_…" }\'</span>'
];
const webhookSample = [
'<span class="text-slate-500">// POST https://yourapp.com/webhooks/cio</span>',
'<span class="text-slate-500">{</span>',
' <span class="text-sky-300">"event"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"certificate.issued"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"orgId"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"org_acme"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"data"</span><span class="text-slate-500">: {</span>',
' <span class="text-sky-300">"learnerId"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"usr_…"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"courseId"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"crs_…"</span><span class="text-slate-500">,</span>',
' <span class="text-sky-300">"certificateUrl"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"https://learn.acme.com/v/CIO-…"</span>',
' <span class="text-slate-500">},</span>',
' <span class="text-sky-300">"createdAt"</span><span class="text-slate-500">:</span> <span class="text-emerald-300">"2026-05-15T12:04:18Z"</span>',
'<span class="text-slate-500">}</span>'
];
const events = [
'user.created',
'enrollment.created',
'enrollment.completed',
'lesson.completed',
'exercise.submitted',
'exercise.graded',
'certificate.issued',
'program.completed',
'course.published',
'course.unpublished'
];
</script>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto max-w-[1100px]">
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<Code2 size={14} class="text-blue-700" />
Developer reference
</Badge>
<h1 class="max-w-[820px] text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
API, Webhooks, MCP.
<em class="text-blue-700 not-italic">No integration tax.</em>
</h1>
<HeroSubtitle>
ClassroomIO ships with a public REST API, signed Webhooks, and an MCP server. No closed marketplace, no "premium
connector" plans. Build the integration that fits your stack.
</HeroSubtitle>
<div class="mt-8 flex flex-wrap items-center gap-3">
<Button href="https://docs.classroomio.com/api">Read the API docs</Button>
<Button href="/github" variant="outline">Star on GitHub</Button>
</div>
</div>
</PageHeader>
<!-- REST API -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-[1fr_1.1fr] lg:gap-20">
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">REST API</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Everything in the LMS is reachable over HTTP.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Bearer-token auth, JSON in, JSON out. Provision learners from your CRM, pull completion data into your
warehouse, or build your own admin tooling on the same surface the dashboard uses.
</p>
<ul class="mt-6 space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Orgs, users, courses, lessons, exercises, submissions, certificates
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Per-org API keys, scoped permissions
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Cursor-based pagination, rate limits documented
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-blue-700">→</span> Same endpoints whether you self-host or run on our cloud
</li>
</ul>
</div>
<CodeBlock
fileName="enrol-a-learner.sh"
lang="bash"
codeLines={apiCurl}
statusBar="POST · application/json · authenticated"
/>
</div>
</section>
<!-- Webhooks -->
<section class="bg-gray-50 px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-[1.1fr_1fr] lg:gap-20">
<CodeBlock
fileName="webhook-payload.json"
lang="JSON"
codeLines={webhookSample}
statusBar="signed with HMAC-SHA256 · retries up to 24h"
/>
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Webhooks</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Subscribe to the events you care about, instead of polling.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
Every important state change posts to your endpoint, signed with your shared secret. We retry with exponential
backoff, and you can replay any event from the dashboard.
</p>
<div class="mt-6 flex flex-wrap gap-2">
{#each events as event}
<span class="rounded-full border border-gray-200 bg-white px-3 py-1 font-mono text-xs text-blue-700">
{event}
</span>
{/each}
</div>
</div>
</div>
</section>
<McpServerSection />
<!-- Self-host -->
<section class="px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto grid max-w-[1100px] grid-cols-1 items-center gap-12 lg:grid-cols-2 lg:gap-20">
<div>
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Self-host</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Or run the whole thing in your own VPC.
</h2>
<p class="mt-5 text-base leading-relaxed text-gray-500">
ClassroomIO is open source on GitHub. You can self-host the dashboard, the API, the AI assistant, and the MCP
server, and bring your own AI keys. It's the same product with the same API surface.
</p>
<div class="mt-8 flex flex-wrap items-center gap-3">
<a
href="https://docs.classroomio.com/self-hosting"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg bg-gray-950 px-5 py-2.5 text-sm font-medium text-white no-underline transition-all hover:-translate-y-0.5 hover:bg-blue-700"
>
Self-hosting guide →
</a>
<a
href="/github"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-5 py-2.5 text-sm font-medium text-gray-950 no-underline transition-all hover:border-gray-950"
>
<Github size={16} />
View the source
</a>
</div>
</div>
<div class="rounded-2xl border border-gray-200 bg-gray-50 p-6">
<p class="text-xs font-medium tracking-widest text-gray-500 uppercase">Stack</p>
<ul class="mt-4 space-y-3 text-sm text-gray-700">
<li class="flex items-start gap-3">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-blue-50 text-[10px] font-medium text-blue-700"
>API</span
>
Hono on Node, Drizzle, Postgres
</li>
<li class="flex items-start gap-3">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-blue-50 text-[10px] font-medium text-blue-700"
>UI</span
>
SvelteKit 2, Svelte 5, Tailwind v4
</li>
<li class="flex items-start gap-3">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-blue-50 text-[10px] font-medium text-blue-700"
>AI</span
>
Bring your own keys for OpenAI, Anthropic, Google Gemini, or Moonshot
</li>
<li class="flex items-start gap-3">
<span
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-md bg-blue-50 text-[10px] font-medium text-blue-700"
>MCP</span
>
Open-source server, <span class="font-mono text-xs">@classroomio/mcp</span>
</li>
</ul>
</div>
</div>
</section>
<PageSignupCTA
header="Programmable from day one."
subText="API, Webhooks, and an MCP server in the box. Self-host it or use the hosted version. Same surface either way."
btnLabel="Read the docs"
link="https://docs.classroomio.com/api"
demo={false}
/>
</section>
@@ -0,0 +1,9 @@
export const prerender = true;
export const load = () => ({
pageMetaTags: {
title: 'Open source LMS API, Webhooks, and MCP server | ClassroomIO',
description:
'Automate course authoring and learner provisioning with the ClassroomIO REST API, Webhooks, and MCP server. Open source, self-hostable, no integration tax.'
}
});
+3 -3
View File
@@ -29,9 +29,9 @@
</div>
<PageSignupCTA
header="Ready To Scale Your Training Efforts?"
subText="It's free to sign up and start getting value out of the product."
btnLabel="Sign me up"
header="Turn what your team already knows into training."
subText="ClassroomIO helps you convert docs, policies, videos, and internal knowledge into courses, quizzes, certificates, and completion evidence."
btnLabel="Start for free"
link="/signup"
demo={false}
/>
@@ -1 +0,0 @@
export const prerender = true;
@@ -1,101 +0,0 @@
<script lang="ts">
import ArrowUpRight from '@lucide/svelte/icons/arrow-up-right';
import { FORM_TYPE } from '$lib/utils/types';
import type { FORM_TYPE_KEY } from '$lib/utils/types';
import HelpForm from '$lib/features/contact/help-form.svelte';
import { PageHeader } from '$lib/components';
import FeedbackForm from '$lib/features/contact/feedback-form.svelte';
let currentForm: FORM_TYPE_KEY = $state();
const formList = [
{
id: FORM_TYPE.BUG,
title: '🐜 I want to report a bug',
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml'
},
{
id: FORM_TYPE.HELP,
title: '🙋‍♂️ I need help'
},
{
id: FORM_TYPE.FEATURE,
title: '📮 I want to request a feature',
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=&projects=&template=feature-request.yml'
},
{
id: FORM_TYPE.FEEDBACK,
title: '💭 I have feedback'
}
];
const onChange = (form: FORM_TYPE_KEY, to?: string) => {
currentForm = form;
if (to) {
window.open(to, '_blank');
}
};
</script>
<svelte:head>
<title>Contact us | ClassroomIO</title>
</svelte:head>
<section>
<PageHeader className="flex flex-col items-center justify-center bg-[#F5F8FE] text-center">
<div class="mb-2 ml-[5%] flex w-full items-center justify-center lg:ml-0">
<span
style="color: rgb(75, 85, 99);"
class="rounded-full border-2 border-[#C2D2FF] bg-[#DCE5FF] px-4 py-1 text-sm font-medium text-blue-700"
>
Contact Us
</span>
</div>
<h1 class="mx-auto flex flex-col items-center text-4xl text-slate-900 md:text-6xl">
<span>Get in touch with <span class="text-blue-700">Us</span></span>
</h1>
<p class="mt-10 w-[90%] max-w-3xl text-center text-lg font-normal text-slate-700 md:w-[60%] lg:mt-7">
Report any bugs you encounter, request new features you'd like to see, or ask any questions you have about
ClassroomIO
</p>
</PageHeader>
<div class="mx-auto my-20">
<!-- Content here -->
<div class="mx-auto grid w-[90%] grid-cols-1 place-items-center gap-10 md:grid-cols-2 lg:w-[60%]">
{#each formList as list}
<button
onclick={() => onChange(list.id, list.to)}
class="w-full rounded-md border-[1.5px] p-3 text-start {currentForm === list.id
? `border-[#0233BD]`
: `border-gray-200`} flex items-center justify-between"
>
<p class="text-lg font-normal text-slate-700">{list.title}</p>
{#if list.to}
<ArrowUpRight size={16} />
{/if}
</button>
{/each}
</div>
{#if currentForm === FORM_TYPE[currentForm]}
<div class="mx-auto w-[90%] px-5 lg:w-[60%]">
{#if currentForm === FORM_TYPE.HELP}
<HelpForm />
{:else if currentForm === FORM_TYPE.FEEDBACK}
<FeedbackForm />
{/if}
</div>
{/if}
<!-- <PageSignupCTA
header="Scale your Teaching Business in Minutes"
subText="Don't wait, let's get you started."
btnLabel="Register"
link="/teach/register"
demo={false}
/> -->
</div>
</section>
@@ -0,0 +1,179 @@
<script lang="ts">
import { HeroSubtitle, McpServerSection, PageHeader, PageSignupCTA } from '$lib/components';
import { Badge } from '@cio/ui/base/badge';
import { Button } from '@cio/ui/base/button';
import Sparkles from '@lucide/svelte/icons/sparkles';
import Copy from '@lucide/svelte/icons/copy';
import Check from '@lucide/svelte/icons/check';
type Recipe = {
eyebrow: string;
title: string;
description: string;
prompt: string;
};
const recipes: Recipe[] = [
{
eyebrow: '01 · Authoring',
title: 'Generate a customer onboarding course from a doc URL',
description:
'The agent plans the course, drafts every lesson, and writes the quiz questions — then waits for your approval before publishing.',
prompt:
'Generate a customer onboarding course from https://docs.acme.com/getting-started. Use the "Customer Onboarding" template. Plan the course first, then wait for my approval before implementing the lessons.'
},
{
eyebrow: '02 · Refresh',
title: 'Refresh a compliance course after a policy update',
description:
'When the policy changes, the agent identifies the affected lessons and drafts the revisions for your review.',
prompt:
'Open the "Annual Security Training" course. Fetch the new policy at https://wiki.acme.com/security/soc2. Update any lessons whose content is contradicted by the policy, tag the course with "needs-review", and report what you changed.'
},
{
eyebrow: '03 · Launches',
title: 'Tag every course tied to a new product launch',
description: 'Bulk-tag drafts and published courses so you can filter, assign, or report on them in one query.',
prompt:
'Find every course in my org whose title or description mentions the v3.2 API. Apply the tag "launch-v3.2" to all of them and tell me how many you tagged.'
},
{
eyebrow: '04 · Quiz authoring',
title: 'Add quiz questions to an existing lesson',
description:
'The agent reads the lesson body and generates assessment questions you can review before publishing.',
prompt:
'In the "Acme 101" course, find the "Connecting your data source" lesson. Add an exercise to it with 5 quiz questions covering the key concepts in the lesson body. Use a mix of multiple choice and short answer.'
},
{
eyebrow: '05 · Reorganise',
title: 'Reorder lessons inside a course',
description: 'Tell the agent what order matters; it reorders sections and lessons without touching the content.',
prompt:
'In the "Admin Bootcamp" course, move the "SSO setup" section before "Inviting your team". Keep all other ordering as-is. Show me the new structure before applying.'
},
{
eyebrow: '06 · Review',
title: 'Audit every draft before publish',
description:
'Get a written summary of what each draft is missing — landing page, quiz questions, certificate rule, or final exam.',
prompt:
'List every draft course in my org. For each one, fetch its structure and tell me what is still missing before it can go live (e.g. landing page copy, final exam, certificate rule, lessons without exercises).'
}
];
let copiedIndex = $state<number | null>(null);
async function copyPrompt(index: number, prompt: string) {
try {
await navigator.clipboard.writeText(prompt);
copiedIndex = index;
setTimeout(() => {
if (copiedIndex === index) copiedIndex = null;
}, 1800);
} catch (e) {
console.error('Copy failed', e);
}
}
</script>
<section class="bg-white">
<PageHeader className="px-6 lg:px-12">
<div class="mx-auto max-w-[1100px]">
<Badge variant="outline" class="mb-6 gap-2! bg-white px-3.5! py-1.5!">
<Sparkles size={14} class="text-blue-700" />
MCP recipes
</Badge>
<h1 class="max-w-[820px] text-[clamp(2rem,3.4vw,2.8rem)] leading-[1.1] font-medium tracking-tight text-gray-950">
Let your agent
<em class="text-blue-700 not-italic">keep your courses up to date.</em>
</h1>
<HeroSubtitle>
Plug the ClassroomIO MCP server into Cursor, Claude Code, Codex, or OpenCode. Then paste one of these prompts.
The agent plans the work, waits for your approval, and runs it.
</HeroSubtitle>
<div class="mt-8 flex flex-wrap items-center gap-3">
<Button href="/signup">Get an MCP key</Button>
<Button href="/automation" variant="outline">Read the docs</Button>
</div>
</div>
</PageHeader>
<!-- Recipe list -->
<section class="bg-white px-6 py-12 lg:px-12 lg:py-16">
<div class="mx-auto max-w-[1100px]">
<div class="mb-8 max-w-[640px]">
<div class="mb-2 text-xs font-medium tracking-widest text-blue-700 uppercase">Recipes</div>
<h2 class="text-[clamp(1.75rem,2.6vw,2.2rem)] leading-[1.15] font-medium tracking-tight">
Paste a prompt. Get the result.
</h2>
</div>
<div class="space-y-3">
{#each recipes as recipe, i}
<div
class="group border-2 border-dotted border-gray-200 bg-white p-5 transition-colors hover:border-blue-700 lg:p-7"
>
<p class="font-mono text-[11px] tracking-widest text-blue-700 uppercase">{recipe.eyebrow}</p>
<h3 class="mt-2 text-base font-medium text-gray-950 lg:text-lg">{recipe.title}</h3>
<p class="mt-1.5 text-sm leading-relaxed text-gray-500">{recipe.description}</p>
<!-- Prompt block -->
<div class="relative mt-5 border border-gray-200 bg-gray-50">
<div class="flex items-center justify-between border-b border-gray-200 bg-white px-3 py-2">
<span class="cio-code-font text-[10px] tracking-widest text-gray-500 uppercase">Prompt</span>
<button
type="button"
onclick={() => copyPrompt(i, recipe.prompt)}
class="cio-code-font inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium tracking-wide opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100 focus:opacity-100 {copiedIndex ===
i
? 'bg-emerald-50 text-emerald-700 opacity-100! ring-1 ring-emerald-200'
: 'bg-blue-50 text-blue-700 ring-1 ring-blue-100 hover:bg-blue-100'}"
aria-label="Copy prompt"
>
{#if copiedIndex === i}
<Check size={12} strokeWidth={2.5} />
Copied
{:else}
<Copy size={12} strokeWidth={2} />
Copy
{/if}
</button>
</div>
<pre
class="cio-code-font overflow-x-auto px-4 py-3.5 text-[12.5px] leading-[1.6] break-words whitespace-pre-wrap text-gray-800">{recipe.prompt}</pre>
</div>
</div>
{/each}
</div>
</div>
</section>
<McpServerSection
eyebrow="Setup"
title="Two minutes to wire it up."
description="Drop this into your agent's MCP config, restart the client, and you're ready to run any of the recipes above."
showTools={false}
/>
<PageSignupCTA
header="One prompt away from a real course."
subText="Spin up a free workspace, paste a recipe, and watch your agent build it."
btnLabel="Get started free"
/>
</section>
<style>
.cio-code-font,
:global(.cio-code-font *) {
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
font-feature-settings:
'liga' 0,
'calt' 0;
font-variant-ligatures: none;
letter-spacing: 0;
}
</style>
@@ -0,0 +1,9 @@
export const prerender = true;
export const load = () => ({
pageMetaTags: {
title: 'MCP recipes for ClassroomIO | Build courses with your agent',
description:
'Ready-to-run prompts for the ClassroomIO MCP server. Generate courses, refresh content from updated docs, tag launches, and more — from Cursor, Claude Code, Codex, or OpenCode.'
}
});
+6 -5
View File
@@ -17,11 +17,12 @@
<section>
<PageHeader className="flex flex-col items-center justify-center text-center">
<h1 class="mx-auto flex flex-col items-center text-4xl text-slate-900 md:text-6xl">
<span>There is room for</span>
<span class="relative text-blue-700">Everyone.</span>
<span>Certified training for</span>
<span class="relative text-blue-700">every team.</span>
</h1>
<p class="mt-10 w-[90%] text-center text-lg font-normal text-slate-700 md:w-[60%] lg:mt-7">
You get a customizable LMS, AI integration for productive educators and many more...
Start with your docs, policies, and videos. Choose the plan that helps you turn them into courses, certificates,
and completion evidence.
</p>
<div class="mt-8">
<PricingToggle
@@ -76,8 +77,8 @@
</section>
<PageSignupCTA
header="Migrate your certification program to ClassroomIO"
subText="Try before you buy. No credit card required."
header="Choose the plan that fits your training program."
subText="Start small, prove completion, and scale certified training across your teams, customers, and partners."
btnLabel="Sign up for free"
link="/signup"
demo={false}
+3 -5
View File
@@ -31,9 +31,7 @@
{@render children?.()}
<PageSignupCTA
header="Scale Your Bootcamp Today with ClassroomIO"
subText="ClassroomIO gives small and medium bootcamps the power of online learning, simplified."
btnLabel="Sign me up"
link="/signup"
demo={false}
header="Stop forwarding help articles."
subText="Launch a branded academy on your domain in days."
btnLabel="Book a Demo"
/>
-21
View File
@@ -1,21 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
interface Props {
url: string;
children?: import('svelte').Snippet;
}
let { url, children }: Props = $props();
</script>
{#key url}
<div class="transition" in:fade>
{@render children?.()}
</div>
{/key}
<style>
.transition {
height: 100%;
}
</style>