mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
414 lines
29 KiB
TypeScript
414 lines
29 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'
|
|
// 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 — chat, or build with dev'
|
|
const SITE_DESCRIPTION = 'The whole cloud in one binary. Build with autonomous agents, frontier models, and sovereign infrastructure. Open source.'
|
|
|
|
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',
|
|
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 2016, through Techstars '17.
|
|
foundingDate: '2016',
|
|
funder: { '@type': 'Organization', name: 'Techstars', sameAs: 'https://www.techstars.com' },
|
|
sameAs: [
|
|
'https://github.com/hanzoai',
|
|
'https://x.com/hanzoai',
|
|
'https://discord.gg/XthHQQj',
|
|
'https://linkedin.com/company/hanzo-ai',
|
|
'https://facebook.com/hanzoinc',
|
|
'https://youtube.com/@HanzoAI',
|
|
'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',
|
|
],
|
|
description: 'Open-source AI platform, autonomous developer agents, and sovereign cloud infrastructure.',
|
|
// 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': 'WebSite',
|
|
'@id': 'https://hanzo.ai/#website',
|
|
url: 'https://hanzo.ai',
|
|
name: 'Hanzo AI',
|
|
description: SITE_DESCRIPTION,
|
|
publisher: {
|
|
'@id': 'https://hanzo.ai/#organization',
|
|
},
|
|
potentialAction: {
|
|
'@type': 'SearchAction',
|
|
target: {
|
|
'@type': 'EntryPoint',
|
|
urlTemplate: 'https://hanzo.ai/search?q={search_term_string}',
|
|
},
|
|
'query-input': 'required name=search_term_string',
|
|
},
|
|
},
|
|
{
|
|
'@type': 'SoftwareApplication',
|
|
'@id': 'https://hanzo.ai/#platform',
|
|
name: 'Hanzo AI Cloud & Dev Platform',
|
|
applicationCategory: 'DeveloperApplication, ArtificialIntelligence',
|
|
operatingSystem: 'Cloud, Linux, macOS, Windows',
|
|
offers: {
|
|
'@type': 'Offer',
|
|
price: '0',
|
|
priceCurrency: 'USD',
|
|
},
|
|
description: 'One API for every model, autonomous agent orchestration, vector storage, IAM, KMS, and serverless functions.',
|
|
},
|
|
],
|
|
}
|
|
|
|
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) }}
|
|
/>
|
|
</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>
|
|
)
|
|
}
|