diff --git a/www/app/(home)/ThemeToggle.tsx b/www/app/(home)/ThemeToggle.tsx
index 2dd97157..0b59ebc8 100644
--- a/www/app/(home)/ThemeToggle.tsx
+++ b/www/app/(home)/ThemeToggle.tsx
@@ -1,11 +1,9 @@
'use client';
import { useEffect, useState } from 'react';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty, so setTheme()
-// silently no-ops. fumadocs re-exports the matching hook for this reason.
-import { useTheme } from 'fumadocs-ui/provider/base';
+// The landing has its own next-themes provider (see layout.tsx), so the hook
+// comes from next-themes directly.
+import { useTheme } from 'next-themes';
// The landing's own theme control.
//
diff --git a/www/app/(home)/Veil.tsx b/www/app/(home)/Veil.tsx
index b944aaaf..5688a1dd 100644
--- a/www/app/(home)/Veil.tsx
+++ b/www/app/(home)/Veil.tsx
@@ -1,9 +1,6 @@
'use client';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty.
-import { useTheme } from 'fumadocs-ui/provider/base';
+import { useTheme } from 'next-themes';
import dynamic from 'next/dynamic';
import { useEffect, useState } from 'react';
diff --git a/www/app/(home)/Wordmark.tsx b/www/app/(home)/Wordmark.tsx
index a551c228..0c75b5f4 100644
--- a/www/app/(home)/Wordmark.tsx
+++ b/www/app/(home)/Wordmark.tsx
@@ -1,11 +1,13 @@
'use client';
import { useEffect, useState } from 'react';
-// Import useTheme from fumadocs, NOT from 'next-themes' directly. fumadocs
-// bundles its own next-themes copy inside RootProvider; a direct import
-// resolves to a second module instance whose context is empty.
-import { useTheme } from 'fumadocs-ui/provider/base';
-import { ParticleText } from './ParticleText';
+import { useTheme } from 'next-themes';
+import dynamic from 'next/dynamic';
+import { LazyIsland } from './islands';
+
+// The particle field is the last thing on a page twelve thousand pixels tall.
+// Its code loads when the footer comes within a screen, not before.
+const ParticleText = dynamic(() => import('./ParticleText').then((m) => m.ParticleText), { ssr: false });
// The wordmark that closes the page.
//
@@ -39,7 +41,7 @@ export function Wordmark() {
const tuning = mounted && resolvedTheme === 'light' ? LIGHT : DARK;
return (
-
+
);
}
diff --git a/www/app/(home)/dashboard-scene.tsx b/www/app/(home)/dashboard-scene.tsx
new file mode 100644
index 00000000..c433026c
--- /dev/null
+++ b/www/app/(home)/dashboard-scene.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { BrowserFrame } from './BrowserFrame';
+import { DashboardShowcase } from './DashboardShowcase';
+import { MacbookScroll } from './MacbookScroll';
+
+// The dashboard showcase and its frame, in one module so the island's single
+// dynamic import brings all of it and none of it lands in the page bundle.
+export function DashboardScene() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/www/app/(home)/home.css b/www/app/(home)/home.css
index c72cb1b1..a765d9eb 100644
--- a/www/app/(home)/home.css
+++ b/www/app/(home)/home.css
@@ -828,3 +828,14 @@ html:not(.dark) .hp-veil canvas {
}
/* No light .hp-veil-scrim override. The scrim derives from --background, which
is already white here. */
+
+/* Scroll-mounted islands (islands.tsx). Each reserves the height its content
+ takes at that breakpoint, measured on the built page, so mounting later
+ moves nothing below it. The dashboard scene is a landscape mock that
+ stacks on phones; the beam diagram is the reverse. */
+.island--dashboard { min-height: 550px; }
+.island--beam { min-height: 556px; }
+@media (min-width: 1024px) {
+ .island--dashboard { min-height: 689px; }
+ .island--beam { min-height: 258px; }
+}
diff --git a/www/app/(home)/islands.tsx b/www/app/(home)/islands.tsx
new file mode 100644
index 00000000..4635b476
--- /dev/null
+++ b/www/app/(home)/islands.tsx
@@ -0,0 +1,75 @@
+'use client';
+
+import dynamic from 'next/dynamic';
+import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
+
+// Scroll-mounted islands for the landing page's visual showcases.
+//
+// These components carry no copy, only a mock UI or a diagram, so nothing is
+// lost by leaving them out of the server-rendered HTML. What is gained: their
+// JavaScript (framer-style animation, the dashboard mock, the beam diagram,
+// the particle wordmark) is neither downloaded nor hydrated until the reader
+// scrolls within a screen of them. Everything with text on this page stays
+// server-rendered and hydrates as before.
+//
+// Each island reserves its height up front so mounting later moves nothing
+// (CLS). The sizes are the components' natural heights at the widths the
+// page is laid out for; see the notes on each.
+
+export function LazyIsland({
+ children,
+ style,
+ className,
+ rootMargin = '600px 0px',
+}: {
+ children: ReactNode;
+ style?: CSSProperties;
+ className?: string;
+ rootMargin?: string;
+}) {
+ const ref = useRef(null);
+ const [near, setNear] = useState(false);
+ useEffect(() => {
+ const el = ref.current;
+ if (!el || near) return;
+ if (typeof IntersectionObserver === 'undefined') {
+ setNear(true);
+ return;
+ }
+ const io = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ setNear(true);
+ io.disconnect();
+ }
+ },
+ { rootMargin },
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, [near, rootMargin]);
+ return (
+
+ {near ? children : null}
+
+ );
+}
+
+const DashboardScene = dynamic(() => import('./dashboard-scene').then((m) => m.DashboardScene), { ssr: false });
+const BeamDiagram = dynamic(() => import('./BeamDiagram').then((m) => m.BeamDiagram), { ssr: false });
+
+export function DashboardIsland() {
+ return (
+
+
+
+ );
+}
+
+export function BeamIsland() {
+ return (
+
+
+
+ );
+}
diff --git a/www/app/(home)/landing-base.css b/www/app/(home)/landing-base.css
new file mode 100644
index 00000000..65c38c32
--- /dev/null
+++ b/www/app/(home)/landing-base.css
@@ -0,0 +1,225 @@
+/* The landing page's base sheet.
+ *
+ * The landing has its own root layout and does not load global.css (Tailwind,
+ * the fumadocs theme and the docs chrome, ~95 KB before compression, of which
+ * the landing used two utilities). What it does need from there: Tailwind's
+ * preflight resets, which home.css and landing.css were written on top of,
+ * and the design tokens, which are copied below so both sheets resolve the
+ * same values. Edit the tokens in global.css first, then mirror here. */
+
+@import 'tailwindcss/preflight.css';
+
+:root {
+ --radius: 0.625rem;
+
+ /* ------------------------------------------------------------------
+ Neutral greyscale, matching payloadcms.com.
+
+ Their whole palette is achromatic: 51 colour tokens, every base step
+ R=G=B, no brand hue anywhere. The only chromatic values on the site are
+ semantic status colours. All the warmth in their hero comes from the
+ background video, not from the theme.
+
+ We keep the neutral surfaces and spend the colour on --primary instead,
+ since we have no hero video to carry it. The tokens that used to hold
+ that video's measured palette (--wave-*, --field-*, and the three
+ opacity values beside them) are gone: nothing referenced them, in CSS
+ or in TSX, after the hero they belonged to was replaced.
+
+ Lightness steps below are read off their base scale:
+ 255 -> 100% 221 -> 88.4% 154 -> 66% 47 -> 25%
+ 245 -> 96.5% 208 -> 84% 101 -> 47.3% 20 -> 11.5%
+ 235 -> 93% 181 -> 75% 74 -> 36.8% 7 -> 4.5%
+ ------------------------------------------------------------------ */
+
+ /* shadcn layer — light */
+ --background: oklch(100% 0 0);
+ --foreground: oklch(18.7% 0 0);
+ --card: oklch(100% 0 0);
+ --card-foreground: oklch(18.7% 0 0);
+ --popover: oklch(100% 0 0);
+ --popover-foreground: oklch(18.7% 0 0);
+ /* Ink, not a hue. --primary drives buttons, active states, the marked phrase
+ and the focus ring, and here it is simply the darkest neutral, inverting to
+ the lightest one on the dark theme. The interface stays monochrome.
+
+ Swapping this for a chromatic value is the single edit that colours the
+ whole site, because everything downstream reads it. */
+ --primary: oklch(18.7% 0 0);
+ --primary-foreground: oklch(100% 0 0);
+ --secondary: oklch(96.5% 0 0);
+ --secondary-foreground: oklch(25% 0 0);
+ --muted: oklch(96.5% 0 0);
+ --muted-foreground: oklch(47.3% 0 0);
+ --accent: oklch(93% 0 0);
+ --accent-foreground: oklch(31% 0 0);
+ /* Destructive keeps its hue. Payload keeps chroma for status too: a neutral
+ palette is a brand decision, not a reason to make errors unreadable. */
+ --destructive: oklch(58% 0.21 25);
+ --border: oklch(88.4% 0 0);
+ --input: oklch(88.4% 0 0);
+ --ring: oklch(47.3% 0 0);
+
+ /* fumadocs layer.
+ *
+ * Aliases, not copies. These were the same sixteen values typed a second
+ * time, with nothing tying the two sets together, so editing one and not the
+ * other was silent and the docs drifted away from everything else.
+ *
+ * Declared once, here, and NOT repeated under .dark. Each one resolves
+ * through a shadcn token that .dark already redefines, so the docs follow
+ * the theme with no second block. A second block is the copy that drifts. */
+ --color-fd-background: var(--background);
+ --color-fd-foreground: var(--foreground);
+ --color-fd-card: var(--card);
+ --color-fd-card-foreground: var(--card-foreground);
+ --color-fd-popover: var(--popover);
+ --color-fd-popover-foreground: var(--popover-foreground);
+ --color-fd-muted: var(--muted);
+ --color-fd-muted-foreground: var(--muted-foreground);
+ --color-fd-secondary: var(--secondary);
+ --color-fd-secondary-foreground: var(--secondary-foreground);
+ --color-fd-border: var(--border);
+ --color-fd-primary: var(--primary);
+ --color-fd-primary-foreground: var(--primary-foreground);
+ --color-fd-ring: var(--ring);
+ --color-fd-accent: var(--accent);
+ --color-fd-accent-foreground: var(--accent-foreground);
+
+ /* ------------------------------------------------------------------
+ The semantic steps shadcn does not provide.
+
+ shadcn gives one --border and one --card. The landing uses four edge
+ weights and three translucent panel washes, so those extra steps are
+ derived here instead of being hardcoded in home.css. That file used to
+ hold them as literals, which is why it needed a whole second copy of
+ itself for light mode.
+ ------------------------------------------------------------------ */
+
+ /* Edge weights. These were rgba(black, a) on white and rgba(white, a) on
+ black, which is exactly a foreground mix, so they carry over without loss.
+ The alphas differ per theme on purpose: a black hairline on white reads
+ heavier than a white one on black at the same alpha. */
+ --rule-1: color-mix(in srgb, var(--foreground) 8%, transparent);
+ --rule-2: color-mix(in srgb, var(--foreground) 13%, transparent);
+ --rule-3: color-mix(in srgb, var(--foreground) 30%, transparent);
+ --rule-frame: color-mix(in srgb, var(--foreground) 14%, transparent);
+ --rule-grid: color-mix(in srgb, var(--foreground) 9%, transparent);
+
+ /* Panel washes. Translucent because they sit over the animated canvas. */
+ --wash-1: color-mix(in srgb, var(--card) 72%, transparent);
+ --wash-2: color-mix(in srgb, var(--card) 86%, transparent);
+ --wash-3: color-mix(in srgb, var(--card) 92%, transparent);
+
+ /* A shadow is an absence of light, not a colour from the palette, so this
+ stays black in both themes and only its strength changes. Deriving it from
+ --foreground would make it white on the dark theme, which is a glow. */
+ --shadow-page: 0 30px 70px rgb(0 0 0 / 0.1);
+
+ /* Syntax colours. Code cannot go monochrome the way the interface can: a
+ single-colour listing reads as a quote block rather than as code. So this
+ is the one part of the theme that stays chromatic while --primary is ink.
+
+ Six hues at one lightness and one chroma, so only the hue varies. Holding L
+ and C is what keeps them distinguishable from each other and readable on
+ the page at the same time.
+
+ --chroma-h is a real anchor rather than a reading of --primary's hue, and
+ that is deliberate. An achromatic --primary has no meaningful hue: the
+ browser serialises it with a floating-point crumb in the b channel, which
+ came out as roughly 112 degrees rather than 0. Anchoring a palette on that
+ is anchoring it on rounding noise. Two knobs are worth the honesty.
+
+ At 25 the ramp runs red, orange, yellow-green, green, blue, magenta, which
+ is close to how most editors colour code already. Move this one number to
+ rotate the whole palette; if --primary is ever given a hue and the code
+ should follow it, set this to the same value. */
+ --chroma-h: 25;
+ /* The one fixed colour in the brand mark: the reconciled cell. See components/brand-mark.tsx. */
+ --brand-violet: #6d4bd4;
+ --chroma-l: 0.48;
+ --chroma-c: 0.15;
+ --chroma-1: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 0));
+ --chroma-2: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 40));
+ --chroma-3: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 80));
+ --chroma-4: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 130));
+ --chroma-5: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 200));
+ --chroma-6: oklch(var(--chroma-l) var(--chroma-c) calc(var(--chroma-h) + 300));
+}
+
+.dark {
+ /* shadcn layer — dark */
+ --background: oklch(4.5% 0 0);
+ --foreground: oklch(100% 0 0);
+ --card: oklch(11.5% 0 0);
+ --card-foreground: oklch(100% 0 0);
+ --popover: oklch(11.5% 0 0);
+ --popover-foreground: oklch(100% 0 0);
+ /* The same ink, inverted. Black primary on a black page would be invisible,
+ so the dark theme takes the lightest neutral and puts the dark one on top. */
+ --primary: oklch(100% 0 0);
+ --primary-foreground: oklch(4.5% 0 0);
+ --secondary: oklch(18.7% 0 0);
+ --secondary-foreground: oklch(93% 0 0);
+ --muted: oklch(18.7% 0 0);
+ --muted-foreground: oklch(66% 0 0);
+ --accent: oklch(25% 0 0);
+ --brand-violet: #a78bfa;
+ --accent-foreground: oklch(84% 0 0);
+ --destructive: oklch(70% 0.19 22);
+ --border: oklch(25% 0 0);
+ --input: oklch(25% 0 0);
+ --ring: oklch(66% 0 0);
+
+ /* No --color-fd-* here on purpose. They alias the shadcn tokens in :root,
+ and every one of those is redefined above, so the docs are already dark.
+ Repeating them here is what made the two sets drift.
+
+ The --rule-*, --wash-* and --chroma-* steps are not repeated either: they
+ are built from --foreground, --card and --primary, all redefined above.
+ Only the values that genuinely differ by theme are listed below. */
+
+ /* Below about 7% a white hairline composites to #0e0e0e on black and reads
+ as nothing, so the dark ground needs its own alphas rather than the ones
+ tuned for paper. */
+ --rule-1: color-mix(in srgb, var(--foreground) 7.5%, transparent);
+ --rule-2: color-mix(in srgb, var(--foreground) 12%, transparent);
+ --rule-3: color-mix(in srgb, var(--foreground) 28%, transparent);
+ --rule-frame: color-mix(in srgb, var(--foreground) 16%, transparent);
+ --rule-grid: color-mix(in srgb, var(--foreground) 10%, transparent);
+
+ --wash-1: color-mix(in srgb, var(--card) 66%, transparent);
+ --wash-2: color-mix(in srgb, var(--card) 72%, transparent);
+ --wash-3: color-mix(in srgb, var(--card) 86%, transparent);
+
+ /* A black page can carry a heavy shadow because it has nowhere to go. White
+ cannot: at 0.8 the screenshots look like they are floating off the sheet. */
+ --shadow-page: 0 30px 70px rgb(0 0 0 / 0.8);
+
+ /* Lifted for the dark ground. Same hues, read against black instead of white. */
+ --chroma-l: 0.78;
+ --chroma-c: 0.13;
+}
+
+html {
+ scrollbar-gutter: stable;
+}
+
+/* No page-level horizontal scroll (clip, never hidden — preserves sticky). */
+html,
+body {
+ overflow-x: clip;
+}
+
+/* The one utility the landing used from Tailwind. */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
diff --git a/www/app/(home)/layout.tsx b/www/app/(home)/layout.tsx
index 7955f1e7..7f1c3614 100644
--- a/www/app/(home)/layout.tsx
+++ b/www/app/(home)/layout.tsx
@@ -1,35 +1,37 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
-import { Inter, JetBrains_Mono } from 'next/font/google';
+import { Inter, JetBrains_Mono, Outfit } from 'next/font/google';
+import Script from 'next/script';
+import { ThemeProvider } from 'next-themes';
+import './landing-base.css';
import './home.css';
import { Veil } from './Veil';
import { GridLines } from './Grid';
import { Header } from './Header';
import { Footer } from './Footer';
-// The landing.
+// The landing, as its own root layout.
+//
+// The rest of the site sits under app/(site) with the fumadocs shell: its
+// provider, its global stylesheet (Tailwind plus the docs theme), its search
+// dialog. The landing used two utilities from that sheet and none of the
+// rest, but paid for all of it on every visit: ~95 KB of CSS before
+// compression and the search bundle in the critical path. Two root layouts
+// let the landing ship only what it draws. Same design tokens (mirrored in
+// landing-base.css), same theme storage key, so the toggle here and the one
+// in the docs stay in step.
//
// Marketing chrome of its own rather than the shared Navbar and Footer: a
// compact hairline header, a four-column footer under a giant wordmark, and
-// three grid lines on the quarter points of the content column. The docs keep
-// the shared shell, so the two deliberately differ.
+// three grid lines on the quarter points of the content column.
//
// Behind all of it, PixelBlast: an fbm field thresholded against an 8x8 Bayer
// matrix, so the backdrop is a grid of hard on/off cells rather than a
// gradient. It has a texture, which suits a page whose own structure is three
-// hairlines. Clicking anywhere sends a ring through it.
-//
-// The canvas is fixed to the viewport rather than sized to the document. One
-// screen of GL covers a page twelve thousand pixels tall, and the field holds
-// still while the page scrolls over it.
-//
-// Everything above it is translucent by a few points in home.css, which lets
-// the violet through the sections instead of only between them.
-//
-// Follows the site light/dark toggle, shared with the docs through the single
-// RootProvider in the root layout. Dark is the set this was drawn in; light is
-// the same composition on white, with the backdrop dropped back and its scrim
-// inverted. See the light block in home.css.
+// hairlines. Clicking anywhere sends a ring through it. The canvas is fixed to
+// the viewport rather than sized to the document, and everything above it is
+// translucent by a few points in home.css, which lets the violet through the
+// sections instead of only between them.
const inter = Inter({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
@@ -40,8 +42,17 @@ const mono = JetBrains_Mono({
weight: ['400', '500'],
variable: '--font-jetbrains-mono',
});
+// Display headings below the fold (landing.css --font-display). Outfit stands
+// in for Gilroy; see the note in app/(site)/layout.tsx.
+const outfit = Outfit({
+ subsets: ['latin'],
+ variable: '--font-outfit',
+});
export const metadata: Metadata = {
+ metadataBase: new URL('https://concile.dev'),
+ alternates: { canonical: './' },
+ verification: { google: 'kiKhCzpdz32bLp_YjGbYMjPcRlpWIlyHkIpXq_zGP9I' },
/* Title and description carry the same words as the H1, because a visitor who
searched one of them should land on a page that repeats it back. "Realtime"
rather than "reactive": reactive is Convex's brand term, realtime is what
@@ -53,18 +64,35 @@ export const metadata: Metadata = {
export default function HomeLayout({ children }: { children: ReactNode }) {
return (
-
-
- {/* Tuned per theme inside Veil rather than filtered here. See Veil.tsx. */}
-
-
-
-
-
-
- {children}
-
-
-
+
+
+ {/* Same settings as fumadocs' RootProvider uses in the (site) layout, so
+ the class on and the "theme" key in storage mean the same
+ thing on both sides. */}
+
+
+
+ {/* Tuned per theme inside Veil rather than filtered here. See Veil.tsx. */}
+
+
+
+
+
+
+ {children}
+
+
+
+
+ {/* Microsoft Clarity, production only, loaded after hydration. The
+ element id must not be "clarity": browsers expose every id as a
+ window global and the loader reads window.clarity. */}
+ {process.env.NODE_ENV === 'production' ? (
+
+ ) : null}
+
+
);
}
diff --git a/www/app/(home)/page.tsx b/www/app/(home)/page.tsx
index 7780797a..2b460512 100644
--- a/www/app/(home)/page.tsx
+++ b/www/app/(home)/page.tsx
@@ -2,10 +2,10 @@ import Link from 'next/link';
import './landing.css';
import { STACK_LOGOS } from './stack-logos';
import { Hero } from './Hero';
+import { BeamIsland, DashboardIsland } from './islands';
import { Reveal } from './Reveal';
import { SpotlightGrid } from './SpotlightGrid';
import { StatNumber } from './StatNumber';
-import { BeamDiagram } from './BeamDiagram';
import { Steps } from './Steps';
import { FeatureCards } from './FeatureCards';
import { Components } from './Components';
@@ -13,9 +13,6 @@ import { Pillars } from './Pillars';
import { ComparisonTable } from './ComparisonTable';
import { Faq } from './Faq';
import { CopyCommand } from './CopyCommand';
-import { DashboardShowcase } from './DashboardShowcase';
-import { BrowserFrame } from './BrowserFrame';
-import { MacbookScroll } from './MacbookScroll';
export default function HomePage() {
return (
@@ -81,11 +78,7 @@ export default function HomePage() {
We include a built-in dashboard that runs on the exact same origin as your app. It uses live subscriptions, meaning you see writes land in the database immediately. We didn't even build a refresh button, because you'll never need it.
-
-
-
-
-
+
{/* ---------------- REACTIVITY DIAGRAM ---------------- */}
@@ -102,7 +95,7 @@ export default function HomePage() {
Read about reactivity
-
+
{/* ---------------- CAPABILITIES (live mini-visuals) ---------------- */}
diff --git a/www/app/(content)/brand/page.tsx b/www/app/(site)/(content)/brand/page.tsx
similarity index 100%
rename from www/app/(content)/brand/page.tsx
rename to www/app/(site)/(content)/brand/page.tsx
diff --git a/www/app/(content)/layout.tsx b/www/app/(site)/(content)/layout.tsx
similarity index 100%
rename from www/app/(content)/layout.tsx
rename to www/app/(site)/(content)/layout.tsx
diff --git a/www/app/api/search/route.ts b/www/app/(site)/api/search/route.ts
similarity index 100%
rename from www/app/api/search/route.ts
rename to www/app/(site)/api/search/route.ts
diff --git a/www/app/blog/[slug]/page.tsx b/www/app/(site)/blog/[slug]/page.tsx
similarity index 100%
rename from www/app/blog/[slug]/page.tsx
rename to www/app/(site)/blog/[slug]/page.tsx
diff --git a/www/app/blog/layout.tsx b/www/app/(site)/blog/layout.tsx
similarity index 100%
rename from www/app/blog/layout.tsx
rename to www/app/(site)/blog/layout.tsx
diff --git a/www/app/blog/page.tsx b/www/app/(site)/blog/page.tsx
similarity index 100%
rename from www/app/blog/page.tsx
rename to www/app/(site)/blog/page.tsx
diff --git a/www/app/compare/[slug]/page.tsx b/www/app/(site)/compare/[slug]/page.tsx
similarity index 100%
rename from www/app/compare/[slug]/page.tsx
rename to www/app/(site)/compare/[slug]/page.tsx
diff --git a/www/app/compare/page.tsx b/www/app/(site)/compare/page.tsx
similarity index 100%
rename from www/app/compare/page.tsx
rename to www/app/(site)/compare/page.tsx
diff --git a/www/app/docs/[[...slug]]/page.tsx b/www/app/(site)/docs/[[...slug]]/page.tsx
similarity index 100%
rename from www/app/docs/[[...slug]]/page.tsx
rename to www/app/(site)/docs/[[...slug]]/page.tsx
diff --git a/www/app/docs/layout.tsx b/www/app/(site)/docs/layout.tsx
similarity index 100%
rename from www/app/docs/layout.tsx
rename to www/app/(site)/docs/layout.tsx
diff --git a/www/app/layout.tsx b/www/app/(site)/layout.tsx
similarity index 99%
rename from www/app/layout.tsx
rename to www/app/(site)/layout.tsx
index 67d5b556..a04a787c 100644
--- a/www/app/layout.tsx
+++ b/www/app/(site)/layout.tsx
@@ -1,5 +1,5 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
-import './global.css';
+import '../global.css';
import type { Metadata } from 'next';
import { Inter, Outfit } from 'next/font/google';
import Script from 'next/script';
diff --git a/www/app/llms-full.txt/route.ts b/www/app/(site)/llms-full.txt/route.ts
similarity index 100%
rename from www/app/llms-full.txt/route.ts
rename to www/app/(site)/llms-full.txt/route.ts
diff --git a/www/app/llms.mdx/docs/[[...slug]]/route.ts b/www/app/(site)/llms.mdx/docs/[[...slug]]/route.ts
similarity index 100%
rename from www/app/llms.mdx/docs/[[...slug]]/route.ts
rename to www/app/(site)/llms.mdx/docs/[[...slug]]/route.ts
diff --git a/www/app/llms.txt/route.ts b/www/app/(site)/llms.txt/route.ts
similarity index 100%
rename from www/app/llms.txt/route.ts
rename to www/app/(site)/llms.txt/route.ts
diff --git a/www/app/og/docs/[...slug]/route.tsx b/www/app/(site)/og/docs/[...slug]/route.tsx
similarity index 100%
rename from www/app/og/docs/[...slug]/route.tsx
rename to www/app/(site)/og/docs/[...slug]/route.tsx
diff --git a/www/bun.lock b/www/bun.lock
index a9db44c9..005555f2 100644
--- a/www/bun.lock
+++ b/www/bun.lock
@@ -15,6 +15,7 @@
"lucide-react": "^1.24.0",
"motion": "^13.3.0",
"next": "16.2.10",
+ "next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.2.7",
"react-dom": "^19.2.7",
diff --git a/www/package.json b/www/package.json
index f6357727..efda72c9 100644
--- a/www/package.json
+++ b/www/package.json
@@ -21,6 +21,7 @@
"lucide-react": "^1.24.0",
"motion": "^13.3.0",
"next": "16.2.10",
+ "next-themes": "^0.4.6",
"ogl": "^1.0.11",
"react": "^19.2.7",
"react-dom": "^19.2.7",