mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
472 lines
33 KiB
TypeScript
472 lines
33 KiB
TypeScript
import type { Metadata, Viewport } from 'next'
|
||
import { colors } from '@hanzo/design'
|
||
import { GuiProvider } from '@/components/GuiProvider'
|
||
import { Providers } from './providers'
|
||
import { ONE_SLASH, STALE } from '@/lib/head'
|
||
import { SEAL } from '@hanzo/app/lib/share'
|
||
import { Toaster } from '@/components/shell/later'
|
||
// NO STYLESHEET OF OUR OWN. Every sheet below is a package's, in cascade order;
|
||
// a page styles itself with gui props and @hanzo/ui components.
|
||
//
|
||
// @hanzo/design's tokens, one published group at a time: the faces, the
|
||
// ladder, type, spacing, radii, elevation, motion and z rungs every Hanzo
|
||
// surface reads.
|
||
import '@hanzo/design/tokens/fonts.css'
|
||
import '@hanzo/design/tokens/colors.css'
|
||
import '@hanzo/design/tokens/typography.css'
|
||
import '@hanzo/design/tokens/spacing.css'
|
||
import '@hanzo/design/tokens/radius.css'
|
||
import '@hanzo/design/tokens/elevation.css'
|
||
import '@hanzo/design/tokens/motion.css'
|
||
import '@hanzo/design/tokens/z.css'
|
||
import './room.css'
|
||
// Its element defaults. The sheet declares its own layer (`base`), so it is
|
||
// imported plain: Next compiles `@import … layer(x)` into `@media layer(x)`, an
|
||
// unknown media feature, and the browser drops the block whole.
|
||
import '@hanzo/design/tokens/base.css'
|
||
// @hanzo/ui's elevation slice: the lit edge and drop shadow `sheet(n)` and
|
||
// `glass(n)` surfaces and every dialog, popover and menu wear. Unlayered, so the
|
||
// lift outranks the utilities. Not `@hanzo/ui/theme.css`, which inlines design's
|
||
// base unlayered and lets its element selectors beat every layered rule.
|
||
import '@hanzo/ui/glass.css'
|
||
// @hanzo/ui's data palette: the hues a record wears, keyed per theme (`.light`).
|
||
// Avatar draws an initial in its record's own hue from these and nothing else,
|
||
// so without them an organization's monogram has no colour.
|
||
import '@hanzo/tokens/css/data'
|
||
// The @hanzo/gui atomic sheet, as a real stylesheet Next can fingerprint and the
|
||
// browser can cache. Generated from lib/gui.ts by `prebuild`; see
|
||
// scripts/gen-gui-css.mjs for why it is not injected at render time.
|
||
import './gui.css'
|
||
// @hanzo/font's faces, italics and pixel cuts. After design's own two faces, so
|
||
// its `font-display: swap` is the declaration that holds for Zen and Zen Mono.
|
||
import '@hanzo/font/css'
|
||
// THE KEYFRAMES THE COMPONENTS NAME. @hanzo/ui's components style themselves
|
||
// through gui props and say how they move with `data-motion` (Pulse, Caret,
|
||
// Skeleton, FadeIn); this sheet holds the keyframes and the reduced-motion
|
||
// switch, which no prop can say. Every selector in it is a data attribute.
|
||
import '@hanzo/ui/styles/motion.css'
|
||
// A long document's type: markdown arrives as bare tags, so `Document` styles
|
||
// it by tag from its own slot. Data attributes again, nothing to collide with.
|
||
import '@hanzo/ui/styles/document.css'
|
||
// Right-to-left, from @hanzo/i18n: under `dir="rtl"` code, URLs, email fields
|
||
// and `[data-i18n-ltr]` keep their own writing order, and a `[data-i18n-mirror]`
|
||
// mark (an arrow, a chevron that points along the line) turns to face the line.
|
||
// Zero specificity, so a component's own rule still wins.
|
||
import '@hanzo/i18n/rtl.css'
|
||
import { ogImages, twitterImages } from '@/lib/constants/og'
|
||
import mark from '@/lib/data/mark.json'
|
||
import Handoff from '@/components/shell/composer'
|
||
import { Route } from '@/components/shell/route'
|
||
import { bootScript } from '@hanzo/appearance/state'
|
||
import * as language from '@hanzo/i18n/boot'
|
||
import { INSTALL } from '@/lib/appearance'
|
||
|
||
|
||
/* viewport-fit=cover: draw under the notch and home indicator instead of
|
||
letterboxing; @hanzo/design's base pays the safe-area insets back where chrome
|
||
and content need them. */
|
||
export const viewport: Viewport = {
|
||
width: 'device-width',
|
||
initialScale: 1,
|
||
viewportFit: 'cover',
|
||
/* Names the UA's own ground before a stylesheet exists, so form controls,
|
||
scrollbars and the canvas behind a slow paint are dark rather than white.
|
||
`dark light` — dark FIRST, because that is this site's default; the second
|
||
value keeps a light-preferring reader's controls legible once the theme
|
||
resolves. Composes with the inline ground rule on <body>: that one paints
|
||
the page, this one paints everything the page does not own. */
|
||
colorScheme: 'dark light',
|
||
themeColor: [
|
||
// A meta tag takes a colour, not a custom property: the design's own values.
|
||
{ media: '(prefers-color-scheme: dark)', color: colors['neutral-950'] },
|
||
{ media: '(prefers-color-scheme: light)', color: colors['neutral-50'] },
|
||
],
|
||
}
|
||
|
||
/**
|
||
* On a touch screen every text field renders at @hanzo/design's control size
|
||
* (`--text-control`, 1rem times the reader's type scale there) and never under
|
||
* 16px: iOS Safari zooms the page in when a focused field computes below 16px,
|
||
* and stays zoomed. design's base asks for the size from a zero-specificity
|
||
* layer rule, which every field that states its own size outranks: the search
|
||
* palette's inline 14px, and @hanzo/ui's Input, Picker and Composer and
|
||
* @hanzo/composer's Field at 13 to 14px. Only an important declaration outranks
|
||
* an inline one. A fine pointer keeps every field as drawn, and pinch-zoom is
|
||
* untouched.
|
||
*/
|
||
const FIELDS =
|
||
'@media (pointer:coarse){input:not([type=checkbox],[type=radio],[type=range],[type=color],[type=file],[type=button],[type=submit],[type=reset],[type=image],[type=hidden]),textarea,select,[contenteditable]:not([contenteditable=false]){font-size:max(16px,var(--text-control,16px))!important}}'
|
||
|
||
/**
|
||
* An icon's URL carries a hash of its own bytes.
|
||
*
|
||
* These are served from the edge with a 24h TTL, and asked for by bare path the
|
||
* cache key never moved when the bytes did — the corrected mark shipped and
|
||
* every visitor kept the old one for a day. `scripts/sync-mark.mjs` derives the
|
||
* hash from each file in `prebuild`, so it cannot be forgotten the way a
|
||
* hand-bumped constant is, and an icon nobody edited keeps its URL and stays
|
||
* cached. Unstamped if the file is unknown to the map: a bare path is the
|
||
* behaviour we already had, and it beats requesting `?v=undefined`.
|
||
*/
|
||
const stamp = (path: keyof typeof mark.icons | string): string => {
|
||
const hash = (mark.icons as Record<string, string>)[path]
|
||
return hash ? `${path}?v=${hash}` : path
|
||
}
|
||
|
||
const SITE_TITLE = 'Hanzo — the AI operating system for business'
|
||
const SITE_DESCRIPTION = "The AI operating system for business: chat, Dev, Studio, agents and one API on every model. Building business software since 2014. Techstars '17. Start free."
|
||
|
||
/** The one line the company says about itself, everywhere it is described. */
|
||
const POSITIONING = "The AI operating system for business. Building business software since 2014. Techstars '17."
|
||
|
||
export const metadata: Metadata = {
|
||
metadataBase: new URL('https://hanzo.ai'),
|
||
title: SITE_TITLE,
|
||
description: SITE_DESCRIPTION,
|
||
// Full modern favicon set from the canonical Hanzo mark (public/favicon.*).
|
||
// Single source of truth — there is no app/icon.* convention file.
|
||
// THE SVG LEADS, AND THE .ico CARRIES NO `sizes`. `sizes="any"` is not a hint
|
||
// that a raster fits every box — it makes the .ico the browser's PREFERRED
|
||
// candidate, outranking a typed SVG whatever the order, so the vector never
|
||
// drew and the tab showed whatever that 5 KB raster was baked from. hanzoai/id
|
||
// hit the identical bug: it swapped the SVG per brand and the swap was
|
||
// invisible for the same reason.
|
||
//
|
||
// Unqualified, the .ico is what a browser with no SVG support falls back to,
|
||
// which is the job it should have had all along.
|
||
icons: {
|
||
icon: [
|
||
{ url: stamp('/favicon.svg'), type: 'image/svg+xml' },
|
||
{ url: stamp('/favicon-32x32.png'), type: 'image/png', sizes: '32x32' },
|
||
{ url: stamp('/favicon-16x16.png'), type: 'image/png', sizes: '16x16' },
|
||
{ url: stamp('/favicon.ico') },
|
||
],
|
||
apple: [{ url: stamp('/apple-touch-icon.png'), sizes: '180x180', type: 'image/png' }],
|
||
},
|
||
manifest: '/site.webmanifest',
|
||
openGraph: {
|
||
title: SITE_TITLE,
|
||
description: SITE_DESCRIPTION,
|
||
url: 'https://hanzo.ai',
|
||
siteName: 'Hanzo AI',
|
||
type: 'website',
|
||
images: ogImages(SITE_TITLE),
|
||
},
|
||
twitter: {
|
||
card: 'summary_large_image',
|
||
title: SITE_TITLE,
|
||
description: SITE_DESCRIPTION,
|
||
images: twitterImages,
|
||
},
|
||
}
|
||
|
||
const JSON_LD = {
|
||
'@context': 'https://schema.org',
|
||
'@graph': [
|
||
{
|
||
'@type': 'Organization',
|
||
'@id': 'https://hanzo.ai/#organization',
|
||
name: 'Hanzo AI',
|
||
// The name before the 2016 rebrand.
|
||
alternateName: 'Crowdstart',
|
||
url: 'https://hanzo.ai',
|
||
// The favicon is 16 and 32 pixels and SVG; a logo entity wants a raster
|
||
// square well past Google's 112-pixel floor, and brand/hanzo-logo.png is
|
||
// 696x696.
|
||
logo: {
|
||
'@type': 'ImageObject',
|
||
url: 'https://hanzo.ai/brand/hanzo-logo.png',
|
||
width: 696,
|
||
height: 696,
|
||
},
|
||
// Stated on /about: started in 2014 (the commerce platform's first commit is
|
||
// 2014-09-29), through Techstars '17.
|
||
foundingDate: '2014',
|
||
funder: { '@type': 'Organization', name: 'Techstars', sameAs: 'https://www.techstars.com' },
|
||
// The two people /about names, with the links /about gives them, and nothing more.
|
||
founder: [
|
||
{
|
||
'@type': 'Person',
|
||
name: 'Antje Worring',
|
||
jobTitle: 'CEO',
|
||
sameAs: ['https://www.linkedin.com/in/antje-worring/', 'https://github.com/zooqueen'],
|
||
},
|
||
{
|
||
'@type': 'Person',
|
||
name: 'Zach Kelling',
|
||
jobTitle: 'CTO',
|
||
sameAs: ['https://www.linkedin.com/in/zeekay/', 'https://github.com/zeekay'],
|
||
},
|
||
],
|
||
// Third-party pages that describe the company, dated.
|
||
subjectOf: [
|
||
{ '@type': 'NewsArticle', headline: "Hanzo Joins Techstars '17", datePublished: '2017-06-08', url: 'https://hanzo.blog/2017-06-08-techstars-17/' },
|
||
{
|
||
'@type': 'NewsArticle',
|
||
headline: 'Techstars Spotlight: Hanzo’s e-commerce tools designed to make you money',
|
||
datePublished: '2017-10-10',
|
||
publisher: { '@type': 'Organization', name: 'Startland News' },
|
||
url: 'https://startlandnews.com/2017/10/techstars-spotlight-hanzos-e-commerce-tools-designed-to-make-you-money/',
|
||
},
|
||
],
|
||
sameAs: [
|
||
'https://github.com/hanzoai',
|
||
'https://x.com/hanzoai',
|
||
'https://discord.gg/XthHQQj',
|
||
'https://linkedin.com/company/hanzo-ai',
|
||
'https://facebook.com/hanzoinc',
|
||
'https://bsky.app/profile/hanzoai.bsky.social',
|
||
'https://www.reddit.com/r/HanzoAI/',
|
||
'https://www.tiktok.com/@hanzoinc',
|
||
'https://www.instagram.com/hanzoinc',
|
||
'https://hanzo.help',
|
||
'https://hanzo.community',
|
||
'https://docs.hanzo.ai',
|
||
'https://hanzo.id',
|
||
'https://platform.hanzo.ai',
|
||
'https://status.hanzo.ai',
|
||
'https://hanzoskills.com',
|
||
],
|
||
slogan: 'The AI operating system for business',
|
||
description: POSITIONING,
|
||
// The addresses /contact and /contact-sales print. A machine asked where to
|
||
// reach a company should not have to find the page first.
|
||
contactPoint: [
|
||
{
|
||
'@type': 'ContactPoint',
|
||
contactType: 'customer support',
|
||
email: '[email protected]',
|
||
url: 'https://hanzo.ai/contact',
|
||
},
|
||
{
|
||
'@type': 'ContactPoint',
|
||
contactType: 'sales',
|
||
email: '[email protected]',
|
||
url: 'https://hanzo.ai/contact-sales',
|
||
},
|
||
{
|
||
'@type': 'ContactPoint',
|
||
contactType: 'security',
|
||
email: '[email protected]',
|
||
url: 'https://hanzo.ai/.well-known/security.txt',
|
||
},
|
||
{ '@type': 'ContactPoint', contactType: 'investor relations', email: '[email protected]', url: 'https://hanzo.ai/investors' },
|
||
{ '@type': 'ContactPoint', contactType: 'sales', url: 'https://cal.com/hanzo' },
|
||
],
|
||
},
|
||
{
|
||
'@type': 'WebSite',
|
||
'@id': 'https://hanzo.ai/#website',
|
||
url: 'https://hanzo.ai',
|
||
name: 'Hanzo AI',
|
||
description: SITE_DESCRIPTION,
|
||
publisher: {
|
||
'@id': 'https://hanzo.ai/#organization',
|
||
},
|
||
// No SearchAction: /search is the Hanzo Search product page and reads no
|
||
// query, so a search box pointing there would answer every query the same.
|
||
},
|
||
{
|
||
'@type': 'SoftwareApplication',
|
||
'@id': 'https://hanzo.ai/#platform',
|
||
name: 'Hanzo',
|
||
applicationCategory: 'BusinessApplication',
|
||
operatingSystem: 'Web, macOS, Windows, Linux',
|
||
url: 'https://hanzo.ai',
|
||
publisher: { '@id': 'https://hanzo.ai/#organization' },
|
||
// The Free plan as GET /v1/billing/plans states it; /pricing reads the rest live.
|
||
offers: {
|
||
'@type': 'Offer',
|
||
name: 'Free',
|
||
price: '0',
|
||
priceCurrency: 'USD',
|
||
url: 'https://hanzo.ai/pricing',
|
||
description: '50 free requests every day on Enso Free and Zen Free, per person. No card.',
|
||
},
|
||
description: 'The AI operating system for business: Chat, Dev, Studio, Tabs, Bot and one API at api.hanzo.ai for every model, Kai decisions and agents, with one key.',
|
||
},
|
||
// The API as schema.org's WebAPI (pending vocabulary). `documentation` is the
|
||
// agent manifest: sign in with a wallet, mint a key, call, top up in USDC.
|
||
{
|
||
'@type': 'WebAPI',
|
||
'@id': 'https://hanzo.ai/#api',
|
||
name: 'Hanzo API',
|
||
url: 'https://api.hanzo.ai/v1',
|
||
documentation: 'https://hanzoskills.com/hanzo.md',
|
||
provider: { '@id': 'https://hanzo.ai/#organization' },
|
||
description: 'One API for every model, Kai decisions and agents. An agent with a wallet signs up, mints a key and tops up in USDC with no human.',
|
||
},
|
||
],
|
||
}
|
||
|
||
export default function RootLayout({
|
||
children,
|
||
}: {
|
||
children: React.ReactNode
|
||
}) {
|
||
// No font loader. Zen is ours, so @hanzo/design ships the two variable woff2
|
||
// files and declares the @font-face itself — this layout imports
|
||
// `tokens/fonts.css` and the faces are loaded and named at :root with nothing
|
||
// to bind. next/font/google could only ever serve a Google-hosted family, and
|
||
// reaching for it here meant a per-build family name that every package
|
||
// stylesheet had to be taught.
|
||
return (
|
||
// `t_dark` is here for the same reason the inline ground rule below is: the
|
||
// gui sheet scopes EVERY token to `:root.t_dark` / `:root.t_light`, and
|
||
// nothing wore either until hydration — so the first paint had no
|
||
// `--borderColor` at all and every gui border fell back to `currentColor`,
|
||
// i.e. a solid white box round the composer, the search field and every
|
||
// input on the page. Dark is only the server's guess; the appearance script
|
||
// in the head paints the reader's choice, else the device's scheme, before
|
||
// the first frame.
|
||
<html lang="en" data-brand="hanzo" suppressHydrationWarning className="t_dark">
|
||
{/* IN THE HEAD, and that is the whole point of it. This is the ground
|
||
before any stylesheet — dev serves CSS through JavaScript and prod can
|
||
race it — and it used to be the first node of the BODY, which is one
|
||
node too late: a browser paints as soon as it has a body to paint, so
|
||
the reader got a frame of default WHITE and then the site. Parsed in
|
||
the head, there is no such frame. The real theme takes over the moment
|
||
the package sheets above land. */}
|
||
<head>
|
||
{/* The reader's language and its direction on <html lang dir> before the
|
||
first paint (@hanzo/i18n): what this browser last showed, else the
|
||
browser's languages. An Arabic page opens right to left instead of
|
||
turning once the bundle runs; the words follow when its catalog loads. */}
|
||
<script dangerouslySetInnerHTML={{ __html: language.bootScript() }} />
|
||
{/* One slash at the front of the address, and a page from before a deploy
|
||
reloaded once when a chunk it names is gone (lib/head.ts). */}
|
||
<script dangerouslySetInnerHTML={{ __html: ONE_SLASH }} />
|
||
{/* The reader's theme, type, spacing, corners and accent as the last
|
||
visit painted them (@hanzo/appearance), before the first frame and
|
||
in both vocabularies the sheets key on: design's `light`, gui's
|
||
`t_light`. A device that has never chosen gets its own scheme
|
||
(`system`: prefers-color-scheme, followed as it changes). */}
|
||
<script dangerouslySetInnerHTML={{ __html: bootScript({ base: INSTALL }) }} />
|
||
<script dangerouslySetInnerHTML={{ __html: STALE }} />
|
||
{/* A shared chat's token out of the address, before anything reads the
|
||
address: the tags below and every module after them see the page
|
||
with no fragment (lib/share.ts `SEAL`). */}
|
||
<script dangerouslySetInnerHTML={{ __html: SEAL }} />
|
||
<script
|
||
dangerouslySetInnerHTML={{
|
||
__html: `if(typeof window!=='undefined'&&(window.__TAURI__||window.__TAURI_INTERNALS__)){document.documentElement.setAttribute('data-desktop','true')}`,
|
||
}}
|
||
/>
|
||
{/* gui's stylesheet is injected into this head while its module
|
||
evaluates — BEFORE hydration — so on the client it holds the slot
|
||
the server wrote this rule into, and React reports the two as one
|
||
element that changed. It is not ours to reorder: the injection
|
||
belongs to the library. Saying so here is what stops React trying to
|
||
reconcile a tag it did not render. */}
|
||
{/* `body` carries the INK here and no ground, which is not a tidy-up:
|
||
html's background propagates to the canvas and is painted before
|
||
every layer, negative ones included, while a background on `body` is
|
||
an ordinary block-level background that paints after them. The
|
||
second copy was therefore an opaque sheet over <DotMatrix/>. The
|
||
touch field size (`FIELDS`) rides the same element. The black
|
||
`--background` is the dark theme's alone (`:root:not(.light)`): a
|
||
bare `:root` rule lands after design's `.light` and turns every
|
||
`var(--background)` black in light, a filled action's ink among them.
|
||
On a phone the app's menu stands behind the pane on that floor
|
||
(`--pane-floor`), and the pane slides over it, raised; both are
|
||
@hanzo/app' own (Drawer.tsx), so no rule here draws them.
|
||
The account menu opens above the account row at the column's foot,
|
||
so it is never taller than the window above that row: the row and
|
||
its gutter take at most 80px of the foot, and the menu keeps the
|
||
gutter or the device's inset clear at the top. What does not fit
|
||
scrolls inside the menu. The rule names the menu's own panel
|
||
(@hanzo/ui's `dropdown-menu-content`): there is no popper wrapper
|
||
around it to size any more, and the panel's own `overflow: hidden`
|
||
would cut the rows below the fold instead of scrolling them.
|
||
The rooms' own phone sidebar (`[data-slot="workspace"]`, on /chat,
|
||
/home and the other rooms) is a drawer over a scrim too, so below
|
||
gui's desk breakpoint (768px) it stands on the same overlay rungs as
|
||
the app's: over the cookie banner, which waits on the banner rung
|
||
until it closes. The app's drawer (@hanzo/app Drawer.tsx) is the
|
||
whole window while it is out — the menu behind, the pane raised over
|
||
it — so the window, fixed on a phone, takes the modal rung for as
|
||
long as the drawer is not shut; shut, it is the page again. */}
|
||
<script
|
||
id="detect-desktop"
|
||
dangerouslySetInnerHTML={{
|
||
__html: `(function(){
|
||
try {
|
||
if (typeof window !== 'undefined' && ((window.__TAURI__ || window.__TAURI_INTERNALS__))) {
|
||
document.documentElement.setAttribute('data-desktop', 'true');
|
||
}
|
||
} catch(e){}
|
||
})();`,
|
||
}}
|
||
/>
|
||
<style
|
||
suppressHydrationWarning
|
||
dangerouslySetInnerHTML={{
|
||
__html:
|
||
'html[data-desktop="true"]{background:transparent !important;background-color:transparent !important;overflow:hidden !important}html[data-desktop="true"] body{background:#000000 !important;background-color:#000000 !important;border-radius:12px !important;border:1px solid rgba(255,255,255,0.14) !important;box-shadow:0 16px 48px rgba(0,0,0,0.7) !important;overflow:hidden !important;height:100vh !important;box-sizing:border-box !important}html[data-desktop="true"].light body{background:#ffffff !important;background-color:#ffffff !important;border:1px solid rgba(0,0,0,0.14) !important}html[data-desktop="true"][data-maximized="true"] body{border-radius:0 !important;border:none !important}html[data-desktop="true"] [data-slot="window"]{height:100% !important;border-radius:12px !important;overflow:hidden !important}html[data-desktop="true"][data-maximized="true"] [data-slot="window"]{border-radius:0 !important}html[data-desktop="true"] header[data-slot="masthead"]{background:#000000 !important;background-color:#000000 !important;border-bottom:none !important;border-bottom-width:0 !important}header[data-slot="masthead"]{border-bottom:none !important;border-bottom-width:0 !important}:root,html{background:#000000 !important;background-color:#000000 !important;color-scheme:dark}body{background:#000000 !important;background-color:#000000 !important;color:#fafafa}:root.light,html.light{background:#fff !important;background-color:#fff !important;color-scheme:light}html.light body{background:#fff !important;background-color:#fff !important;color:#0a0a0a}:root:not(.light){--background:#000000;--pane-floor:#000000}@media (max-width:768px){[data-slot="frame"]{padding:0 !important;gap:0 !important}:root:not(.light) [data-slot="frame"]{background:#000000 !important;background-image:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(255,255,255,0.04),transparent 70%),linear-gradient(180deg,#000000 0%,#030303 50%,#080808 100%) !important}[data-slot="pane"]{border-radius:0 !important;border-width:0 !important;box-shadow:none !important;background:transparent !important}}@media (max-width:767.98px){[data-slot="workspace"]>[data-slot="scrim"]{z-index:var(--z-overlay) !important}[data-slot="workspace"]>[data-slot="list"]{z-index:var(--z-modal) !important}[data-slot="window"]:has([data-slot="drawer"]:not([data-state="shut"])){z-index:var(--z-modal) !important}}[data-slot="dropdown-menu-content"][role="menu"][aria-label="Account"]{max-height:calc(100dvh - 80px - max(8px,var(--safe-top)) - max(8px,var(--safe-bottom))) !important;overflow-y:auto !important;overscroll-behavior:contain}[data-audience] button,[data-audience] [role="radio"],[data-audience] div{border-radius:9999px !important}[data-slot="action"],[data-slot="chip"]{border-radius:9999px !important}[data-slot="action"][data-join="start"]{border-start-end-radius:0 !important;border-end-end-radius:0 !important}[data-slot="action"][data-join="end"]{border-start-start-radius:0 !important;border-end-start-radius:0 !important}@property --w{syntax:"<length>";inherits:true;initial-value:420px}[data-measure]{--w:min(420px,calc(100vw - 2 * var(--page-gutter)))}[data-measure]>[data-slot="lede"]{max-width:var(--w) !important;font-size:max(14px,calc(var(--w) / 26.25)) !important}[data-fit]>[data-line]{display:block;white-space:nowrap;font-weight:500;letter-spacing:-0.012em}[data-fit]>[data-line="1"]{font-size:calc(var(--w) / 10.24);line-height:1.08}[data-fit]>[data-line="2"]{font-size:calc(var(--w) / 6.13);line-height:.98}[data-flash]{--ink:100%;--x:-9999px;--lo:color-mix(in oklab,var(--foreground) var(--ink),var(--background));color:transparent;-webkit-text-fill-color:transparent;background-image:linear-gradient(90deg,var(--lo) calc(var(--x) - 1em),var(--foreground) var(--x),var(--lo) calc(var(--x) + 1em));-webkit-background-clip:text;background-clip:text;cursor:default}[data-shine]::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(105deg,transparent 30%,color-mix(in oklab,var(--pure-white) 55%,transparent) 50%,transparent 70%);transform:translateX(-120%);animation:shine 5s ease-in-out 1.4s infinite}@keyframes shine{0%{transform:translateX(-120%)}30%,100%{transform:translateX(120%)}}[data-shine]>[data-spark]{display:inline-block;animation:twinkle 2.6s ease-in-out infinite}@keyframes twinkle{0%,100%{transform:scale(1) rotate(0deg);opacity:1}50%{transform:scale(.8) rotate(20deg);opacity:.65}}[data-shine]>svg:last-child{transition:transform .2s ease}[data-shine]:hover>svg:last-child{transform:translateX(2px)}[data-shine]:hover>svg:last-child:dir(rtl){transform:translateX(-2px) scaleX(-1)}@media (prefers-reduced-motion:reduce){[data-shine]::after{display:none}[data-shine]>[data-spark],[data-shine]>svg{animation:none;transition:none}}[data-hue="claudecode"] svg{color:#D97757}[data-models]{display:flex !important;flex-direction:row !important;gap:10px;height:520px}[data-models]>[data-model]{flex:1 1 0 !important;min-width:0;transition:flex-grow .6s cubic-bezier(.2,.8,.2,1)}[data-models]:hover>[data-model]:hover,[data-models]>[data-model]:focus-visible,[data-models]:not(:hover):not(:focus-within)>[data-model][data-lead]{flex-grow:6 !important}[data-model] video[data-media]{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}[data-model]>[data-open]{opacity:0;transform:translateY(10px);transition:opacity .3s,transform .3s}[data-models]:hover>[data-model]:hover>[data-open],[data-models]>[data-model]:focus-visible>[data-open],[data-models]:not(:hover):not(:focus-within)>[data-model][data-lead]>[data-open]{opacity:1;transform:none;transition-delay:.18s}[data-model]>[data-media]{transition:transform 1.4s cubic-bezier(.2,.8,.2,1)}[data-models]:hover>[data-model]:hover>[data-media],[data-models]>[data-model]:focus-visible>[data-media],[data-models]:not(:hover):not(:focus-within)>[data-model][data-lead]>[data-media]{transform:scale(1.04)}[data-model]>[data-spine]{transition:opacity .2s}[data-models]:hover>[data-model]:hover>[data-spine],[data-models]>[data-model]:focus-visible>[data-spine],[data-models]:not(:hover):not(:focus-within)>[data-model][data-lead]>[data-spine]{opacity:0}@media (max-width:1023px){[data-models]{height:auto;overflow-x:auto !important;scroll-snap-type:x mandatory;padding-bottom:8px}[data-models]>[data-model]{flex:0 0 80% !important;height:440px;scroll-snap-align:start}[data-model]>[data-open]{opacity:1;transform:none}[data-model]>[data-spine]{display:none}}@media (prefers-reduced-motion:reduce){[data-models]>[data-model],[data-model]>[data-open],[data-model]>[data-spine],[data-model]>[data-media]{transition:none;transform:none}}[data-typing]>*{animation:pulse 1.1s ease-in-out infinite}[data-typing]>*:nth-child(2){animation-delay:.18s}[data-typing]>*:nth-child(3){animation-delay:.36s}[data-spin]{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){[data-typing]>*,[data-spin]{animation:none}}[data-slot="beta"]{--tilt:.5;position:relative;isolation:isolate;overflow:hidden;background:radial-gradient(50% 160% at 50% 50%,rgba(255,255,255,.045),transparent 72%),linear-gradient(180deg,#1b1c1e 0,#121314 3px,#26282a 4px,#54575b 5px,#2e3033 6px,#1a1b1d 10px,#0e0f10 38%,#121315 62%,#1e1f22 78%,#191a1c calc(100% - 7px),#3e4044 calc(100% - 3px),#585b5f calc(100% - 1px));box-shadow:inset 0 1px 0 rgba(255,255,255,.06),inset 0 -1px 0 rgba(255,255,255,.78)}[data-slot="beta"]::after{content:"";position:absolute;top:0;bottom:0;left:50%;width:240px;margin-left:-120px;z-index:var(--z-base);pointer-events:none;background:linear-gradient(110deg,transparent calc(50% - 100px),rgba(255,255,255,.05) calc(50% - 40px),rgba(255,255,255,.2) calc(50% - 6px),rgba(255,255,255,.62) calc(50% - 1px),rgba(255,255,255,.62) calc(50% + 1px),rgba(255,255,255,.2) calc(50% + 6px),rgba(255,255,255,.05) calc(50% + 40px),transparent calc(50% + 100px));-webkit-mask-image:linear-gradient(180deg,#000 0,#000 3px,rgba(0,0,0,.52) 6px,rgba(0,0,0,.52) calc(100% - 6px),#000 calc(100% - 3px));mask-image:linear-gradient(180deg,#000 0,#000 3px,rgba(0,0,0,.52) 6px,rgba(0,0,0,.52) calc(100% - 6px),#000 calc(100% - 3px));transform:translate3d(calc((var(--tilt) - .5) * 30vw),0,0);will-change:transform}[data-slot="beta"]>*{position:relative;z-index:calc(var(--z-base) + 1);text-shadow:0 1px 1px rgba(0,0,0,.65)}[data-slot="beta"]:is(a):hover svg:last-of-type{transform:translateX(3px)}[data-slot="beta"] svg{transition:transform .25s cubic-bezier(.2,.7,.2,1)}@media (prefers-reduced-motion:reduce){[data-slot="beta"]::after{transform:none}[data-slot="beta"] svg{transition:none}}[data-marquee]{display:flex !important;flex-direction:row !important;width:max-content;animation:marquee 48s linear infinite}[data-marquee-wrap]{-webkit-mask-image:linear-gradient(90deg,transparent,black 10%,black 90%,transparent);mask-image:linear-gradient(90deg,transparent,black 10%,black 90%,transparent)}[data-marquee-wrap]:hover [data-marquee]{animation-play-state:paused}@keyframes marquee{to{transform:translate3d(-50%,0,0)}}@media (prefers-reduced-motion:reduce){[data-marquee]{animation:none;width:auto}[data-marquee]>[aria-hidden]{display:none !important}[data-marquee]>*:first-child{flex-wrap:wrap;justify-content:center;row-gap:24px}[data-marquee-wrap]{-webkit-mask-image:none;mask-image:none}}[data-hue="codex"] svg{fill:url(#hue-codex)}[data-hue="antigravity"] svg{fill:url(#hue-antigravity)}' +
|
||
FIELDS,
|
||
}}
|
||
/>
|
||
<script
|
||
type="application/ld+json"
|
||
dangerouslySetInnerHTML={{ __html: JSON.stringify(JSON_LD) }}
|
||
/>
|
||
{/* What an agent reads first, named in every page's head: the site's
|
||
llms.txt (also as `describedby`, the relation the API's Link header
|
||
and every static site use), /skill.md as the markdown alternate, the
|
||
agent start page, the API catalog (RFC 9727) and the OpenAPI
|
||
document (RFC 8631 service-desc). Here rather than in `metadata`,
|
||
because a page's own `alternates` replaces the root's. */}
|
||
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
|
||
<link rel="describedby" type="text/plain" href="/llms.txt" />
|
||
<link rel="alternate" type="text/markdown" href="/skill.md" title="skill.md" />
|
||
<link rel="help" href="/agents" title="For agents: sign up free, pay as you go" />
|
||
<link rel="api-catalog" href="/.well-known/api-catalog" />
|
||
<link rel="service-desc" type="application/json" href="https://api.hanzo.ai/v1/openapi.json" />
|
||
</head>
|
||
{/* No class. The ground, the ink and the smoothing are @hanzo/design's
|
||
base; carrying them here as three utilities was the same three
|
||
declarations said twice.
|
||
|
||
Content does not depend on scripting, and no rule rescues it:
|
||
components/motion.tsx ships every reveal opaque and in place, and a
|
||
keyed entrance (`data-motion`) is a CSS animation that plays without
|
||
JavaScript. Measured with scripting off on every top-level route,
|
||
nothing a scripted reader sees is hidden. */}
|
||
<body>
|
||
<GuiProvider>
|
||
<Route>
|
||
{/* The shell: which mode you are in, and the field that starts one.
|
||
Both live here so every page has them, and both read the session
|
||
Providers resolves, so they are inside it. */}
|
||
<Providers>
|
||
{children}
|
||
<Handoff />
|
||
</Providers>
|
||
{/* One themed Toaster for the whole document. The account and
|
||
dashboard actions fire toast.success/error and had nowhere to
|
||
land — no Toaster was mounted anywhere, so every one of those
|
||
confirmations was silent. Through @hanzo/ui, so the design
|
||
system owns the material rather than a bare sonner; fetched
|
||
once React owns the page, since a toast is always something
|
||
the reader did after it (components/shell/later.tsx). */}
|
||
<Toaster />
|
||
</Route>
|
||
</GuiProvider>
|
||
{/* No analytics tag here: pageviews AND interaction autocapture already ride
|
||
the ONE @hanzo/event client in <Providers> (host + ingest key + consent
|
||
gate in one place). A second hz.js tag posted to the same /v1/event endpoint
|
||
and double-counted every pageview.
|
||
|
||
NOR an authoring widget. `hanzo.app/edit.js` used to load here, and it
|
||
is an AUTHORING tool on a PUBLISHED page: a shadow-root launcher pinned
|
||
bottom-right at z-index 2147483000, which on a phone landed on 43x32px
|
||
of the drawer's "Try Hanzo" button — this site's one primary action —
|
||
so the edge of the CTA opened an editor instead. Two rules in
|
||
the site stylesheet were written to live with it, a z-index clamp and 64px of
|
||
dock clearance, and both are gone with it. A reader of hanzo.ai is not
|
||
editing hanzo.ai; the entry point for that is the repo. */}
|
||
</body>
|
||
</html>
|
||
)
|
||
}
|