aboutsummaryrefslogtreecommitdiff
path: root/landing
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2026-04-09 15:22:43 +0800
committerbenj <benj@rse8.com>2026-04-09 15:22:43 +0800
commit5f56e0eaf8030f78077fc93b441296bb51fe9331 (patch)
treeab2bad3b7fb1e5bf40d4130f7a158d0ca7ddb011 /landing
parent862216a47ae42ec831d4c2cf0de18225f6971de5 (diff)
downloadtidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar.gz
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar.bz2
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar.lz
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar.xz
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.tar.zst
tidyindex-5f56e0eaf8030f78077fc93b441296bb51fe9331.zip
Add initial Hugo landing page
First pass at the tidyindex.com marketing site, built with a custom minimal Hugo theme — light palette (white/soft blue), Inter + JetBrains Mono, hero with a JSON code preview, format/audience/value sections, and a single-button CTA. Also adds a top-level .gitignore for local Claude/IDE state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'landing')
-rw-r--r--landing/.gitignore4
-rw-r--r--landing/content/_index.md4
-rw-r--r--landing/hugo.toml19
-rw-r--r--landing/layouts/_default/baseof.html32
-rw-r--r--landing/layouts/index.html223
-rw-r--r--landing/layouts/partials/footer.html16
-rw-r--r--landing/layouts/partials/header.html20
-rw-r--r--landing/static/css/style.css637
-rw-r--r--landing/static/favicon.svg6
-rw-r--r--landing/static/robots.txt4
10 files changed, 965 insertions, 0 deletions
diff --git a/landing/.gitignore b/landing/.gitignore
new file mode 100644
index 0000000..9623b27
--- /dev/null
+++ b/landing/.gitignore
@@ -0,0 +1,4 @@
+# Hugo build output
+public/
+resources/
+.hugo_build.lock
diff --git a/landing/content/_index.md b/landing/content/_index.md
new file mode 100644
index 0000000..53f3714
--- /dev/null
+++ b/landing/content/_index.md
@@ -0,0 +1,4 @@
+---
+title: "Tidy Index"
+description: "Clean, well-structured data delivered through APIs built for humans, machines, and LLM agents."
+---
diff --git a/landing/hugo.toml b/landing/hugo.toml
new file mode 100644
index 0000000..82cb835
--- /dev/null
+++ b/landing/hugo.toml
@@ -0,0 +1,19 @@
+baseURL = "https://tidyindex.com/"
+languageCode = "en-us"
+title = "Tidy Index — Structured data, served simply"
+enableRobotsTXT = true
+disableKinds = ["taxonomy", "term"]
+
+[params]
+ description = "Clean, well-structured data delivered through APIs built for humans, machines, and LLM agents."
+ tagline = "Structured data, served simply."
+
+[markup]
+ [markup.goldmark]
+ [markup.goldmark.renderer]
+ unsafe = true
+
+[minify]
+ disableHTML = false
+ disableCSS = false
+ disableJS = false
diff --git a/landing/layouts/_default/baseof.html b/landing/layouts/_default/baseof.html
new file mode 100644
index 0000000..89a0bd7
--- /dev/null
+++ b/landing/layouts/_default/baseof.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="{{ .Site.LanguageCode | default "en" }}">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} — {{ .Site.Title }}{{ end }}</title>
+ <meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" />
+
+ <meta property="og:title" content="{{ .Site.Title }}" />
+ <meta property="og:description" content="{{ .Site.Params.description }}" />
+ <meta property="og:type" content="website" />
+ <meta property="og:url" content="{{ .Site.BaseURL }}" />
+
+ <meta name="twitter:card" content="summary_large_image" />
+ <meta name="twitter:title" content="{{ .Site.Title }}" />
+ <meta name="twitter:description" content="{{ .Site.Params.description }}" />
+
+ <link rel="preconnect" href="https://fonts.googleapis.com">
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
+
+ <link rel="stylesheet" href="{{ "css/style.css" | relURL }}" />
+ <link rel="icon" type="image/svg+xml" href="{{ "favicon.svg" | relURL }}" />
+</head>
+<body>
+ {{ partial "header.html" . }}
+ <main>
+ {{ block "main" . }}{{ end }}
+ </main>
+ {{ partial "footer.html" . }}
+</body>
+</html>
diff --git a/landing/layouts/index.html b/landing/layouts/index.html
new file mode 100644
index 0000000..231ff61
--- /dev/null
+++ b/landing/layouts/index.html
@@ -0,0 +1,223 @@
+{{ define "main" }}
+
+<section class="hero">
+ <div class="container hero-inner">
+ <span class="eyebrow">Clean data, on tap</span>
+ <h1 class="hero-title">
+ Structured data,<br />
+ <span class="accent">served simply.</span>
+ </h1>
+ <p class="hero-sub">
+ Tidy Index delivers clean, well-organized datasets through a single API
+ designed for humans, machines, and LLM agents alike.
+ </p>
+ <div class="hero-cta">
+ <a href="#contact" class="btn btn-primary">Request access</a>
+ <a href="#how" class="btn btn-ghost">See how it works</a>
+ </div>
+
+ <div class="hero-visual" aria-hidden="true">
+ <div class="code-card">
+ <div class="code-card-head">
+ <span class="dot dot-a"></span>
+ <span class="dot dot-b"></span>
+ <span class="dot dot-c"></span>
+ <span class="code-card-title">GET&nbsp; /v1/datasets/cities</span>
+ </div>
+<pre class="code-card-body"><code><span class="tk-p">{</span>
+ <span class="tk-k">"id"</span>: <span class="tk-s">"cities"</span>,
+ <span class="tk-k">"updated"</span>: <span class="tk-s">"2026-04-09T08:14:00Z"</span>,
+ <span class="tk-k">"records"</span>: <span class="tk-p">[</span>
+ <span class="tk-p">{</span> <span class="tk-k">"name"</span>: <span class="tk-s">"Lisbon"</span>, <span class="tk-k">"population"</span>: <span class="tk-n">548703</span> <span class="tk-p">}</span>,
+ <span class="tk-p">{</span> <span class="tk-k">"name"</span>: <span class="tk-s">"Kyoto"</span>, <span class="tk-k">"population"</span>: <span class="tk-n">1463723</span> <span class="tk-p">}</span>,
+ <span class="tk-p">{</span> <span class="tk-k">"name"</span>: <span class="tk-s">"Quito"</span>, <span class="tk-k">"population"</span>: <span class="tk-n">2011388</span> <span class="tk-p">}</span>
+ <span class="tk-p">]</span>
+<span class="tk-p">}</span></code></pre>
+ </div>
+ </div>
+ </div>
+</section>
+
+<section id="what" class="section">
+ <div class="container narrow">
+ <span class="eyebrow center">What is Tidy Index</span>
+ <h2 class="section-title center">We&rsquo;ve already done the cleanup.</h2>
+ <p class="section-lede center">
+ Scraping. Reformatting. Reconciling. Re-scraping when the source changes
+ shape. We do all of that &mdash; once, properly &mdash; so you don&rsquo;t
+ have to do any of it.
+ </p>
+ </div>
+</section>
+
+<section id="formats" class="section section-soft">
+ <div class="container">
+ <span class="eyebrow center">Pick your shape</span>
+ <h2 class="section-title center">One dataset. Every shape you need.</h2>
+ <p class="section-lede center">
+ Ask for JSON, get JSON. Ask for chunks, get chunks. Same data underneath &mdash;
+ no reformatting, no glue code.
+ </p>
+
+ <div class="format-grid">
+ <article class="format-card">
+ <div class="format-icon" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M8 4 4 12l4 8"/>
+ <path d="m16 4 4 8-4 8"/>
+ </svg>
+ </div>
+ <h3>JSON</h3>
+ <p>Stable schemas, stable IDs, ETags on every response. Drop it straight into your code.</p>
+ </article>
+
+ <article class="format-card">
+ <div class="format-icon" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M4 4h16v16H4z"/>
+ <path d="M4 9h16"/>
+ <path d="M9 4v16"/>
+ </svg>
+ </div>
+ <h3>XML &amp; CSV</h3>
+ <p>For the pipelines that have been around longer than your team has. Strict, well-formed, no surprises.</p>
+ </article>
+
+ <article class="format-card">
+ <div class="format-icon" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
+ <circle cx="12" cy="12" r="8"/>
+ <path d="M12 8v4l2 2"/>
+ </svg>
+ </div>
+ <h3>LLM chunks</h3>
+ <p>Already chunked, already cited, already embedding-ready. Drop them into your retriever and stop fighting tokenizers.</p>
+ </article>
+
+ <article class="format-card">
+ <div class="format-icon" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M3 12h4l3-8 4 16 3-8h4"/>
+ </svg>
+ </div>
+ <h3>Streaming</h3>
+ <p>Webhooks and server-sent events the moment something changes. No polling, no stale caches.</p>
+ </article>
+ </div>
+ </div>
+</section>
+
+<section id="how" class="section">
+ <div class="container">
+ <span class="eyebrow center">Who it&rsquo;s for</span>
+ <h2 class="section-title center">Same data. Three kinds of readers.</h2>
+ <p class="section-lede center">
+ People, programs, and agents all want different things from the same
+ dataset. Tidy Index gives each of them what they expect.
+ </p>
+
+ <div class="audience-grid">
+ <div class="audience-card">
+ <div class="audience-num">01</div>
+ <h3>For humans</h3>
+ <p>
+ A real web UI. Skim a schema, preview a few rows, grab a snapshot,
+ and copy a working curl command without leaving the page.
+ </p>
+ </div>
+
+ <div class="audience-card audience-card-feature">
+ <div class="audience-num">02</div>
+ <h3>For computers</h3>
+ <p>
+ Boring REST, the way you like it. OpenAPI specs, semver, ETags,
+ idempotent reads, and rate limits we&rsquo;ll actually tell you about.
+ </p>
+ </div>
+
+ <div class="audience-card">
+ <div class="audience-num">03</div>
+ <h3>For LLM agents</h3>
+ <p>
+ Endpoints shaped like tool calls. Chunks shaped like context. Every
+ response carries its own provenance, so the model never has to guess
+ where the data came from.
+ </p>
+ </div>
+ </div>
+ </div>
+</section>
+
+<section class="section section-soft">
+ <div class="container narrow">
+ <span class="eyebrow center">Why Tidy Index</span>
+ <h2 class="section-title center">Less wrangling. More building.</h2>
+
+ <ul class="value-list">
+ <li>
+ <span class="value-check" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="20 6 9 17 4 12"/>
+ </svg>
+ </span>
+ <div>
+ <strong>Curated, not crawled.</strong>
+ <p>A human looks at every dataset before it ships. We&rsquo;d rather have ten that are right than ten thousand that are almost right.</p>
+ </div>
+ </li>
+ <li>
+ <span class="value-check" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="20 6 9 17 4 12"/>
+ </svg>
+ </span>
+ <div>
+ <strong>Schemas that don&rsquo;t move under you.</strong>
+ <p>Versioned endpoints, deprecation windows, and a changelog you can subscribe to. If something&rsquo;s about to break, you&rsquo;ll be the first to know.</p>
+ </div>
+ </li>
+ <li>
+ <span class="value-check" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="20 6 9 17 4 12"/>
+ </svg>
+ </span>
+ <div>
+ <strong>Every record has a receipt.</strong>
+ <p>Each row links back to where it came from. Your auditors will love it. Your models will stop making things up.</p>
+ </div>
+ </li>
+ <li>
+ <span class="value-check" aria-hidden="true">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="20 6 9 17 4 12"/>
+ </svg>
+ </span>
+ <div>
+ <strong>One key, every format.</strong>
+ <p>JSON, XML, CSV, or LLM chunks. Flip a header &mdash; same data, different shape.</p>
+ </div>
+ </li>
+ </ul>
+ </div>
+</section>
+
+<section id="contact" class="section">
+ <div class="container narrow">
+ <div class="cta-card">
+ <span class="cta-glow" aria-hidden="true"></span>
+ <h2 class="cta-title">Ready when you are.</h2>
+ <a href="mailto:hello@tidyindex.com" class="btn btn-primary btn-lg cta-btn">
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
+ <circle cx="7.5" cy="15.5" r="5.5"/>
+ <path d="m11.5 11.5 9.5-9.5"/>
+ <path d="m17 5 3 3"/>
+ <path d="m14 8 3 3"/>
+ </svg>
+ API Key
+ </a>
+ </div>
+ </div>
+</section>
+
+{{ end }}
diff --git a/landing/layouts/partials/footer.html b/landing/layouts/partials/footer.html
new file mode 100644
index 0000000..d6588fe
--- /dev/null
+++ b/landing/layouts/partials/footer.html
@@ -0,0 +1,16 @@
+<footer class="site-footer">
+ <div class="container footer-inner">
+ <div class="footer-brand">
+ <span class="brand-mark" aria-hidden="true">
+ <svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="4" y="6" width="24" height="3" rx="1.5" fill="currentColor"/>
+ <rect x="4" y="14" width="18" height="3" rx="1.5" fill="currentColor" opacity="0.7"/>
+ <rect x="4" y="22" width="12" height="3" rx="1.5" fill="currentColor" opacity="0.4"/>
+ </svg>
+ </span>
+ <span>Tidy Index</span>
+ </div>
+ <p class="footer-tagline">Structured data, served simply.</p>
+ <p class="footer-meta">&copy; {{ now.Year }} Tidy Index. All rights reserved.</p>
+ </div>
+</footer>
diff --git a/landing/layouts/partials/header.html b/landing/layouts/partials/header.html
new file mode 100644
index 0000000..5f6d85b
--- /dev/null
+++ b/landing/layouts/partials/header.html
@@ -0,0 +1,20 @@
+<header class="site-header">
+ <div class="container header-inner">
+ <a href="{{ "/" | relURL }}" class="brand" aria-label="Tidy Index home">
+ <span class="brand-mark" aria-hidden="true">
+ <svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <rect x="4" y="6" width="24" height="3" rx="1.5" fill="currentColor"/>
+ <rect x="4" y="14" width="18" height="3" rx="1.5" fill="currentColor" opacity="0.7"/>
+ <rect x="4" y="22" width="12" height="3" rx="1.5" fill="currentColor" opacity="0.4"/>
+ </svg>
+ </span>
+ <span class="brand-name">Tidy Index</span>
+ </a>
+ <nav class="site-nav" aria-label="Primary">
+ <a href="#what">What</a>
+ <a href="#formats">Formats</a>
+ <a href="#how">How it works</a>
+ <a href="#contact" class="nav-cta">Get access</a>
+ </nav>
+ </div>
+</header>
diff --git a/landing/static/css/style.css b/landing/static/css/style.css
new file mode 100644
index 0000000..30665f6
--- /dev/null
+++ b/landing/static/css/style.css
@@ -0,0 +1,637 @@
+/* ----------------------------------------------------------------
+ Tidy Index — landing styles
+ Palette: whites, soft blues, generous space.
+---------------------------------------------------------------- */
+
+:root {
+ --c-bg: #ffffff;
+ --c-bg-soft: #f6f9fc;
+ --c-bg-tint: #eef4fb;
+ --c-border: #e3ecf5;
+ --c-border-soft: #eef2f7;
+
+ --c-ink: #0b1f3a;
+ --c-ink-soft: #3b4f6b;
+ --c-ink-mute: #6b7c93;
+
+ --c-blue: #3b82f6;
+ --c-blue-deep: #2563eb;
+ --c-blue-soft: #dbeafe;
+ --c-blue-tint: #eef4ff;
+
+ --c-accent: #60a5fa;
+
+ --shadow-sm: 0 1px 2px rgba(15, 38, 73, 0.04);
+ --shadow-md: 0 6px 24px -8px rgba(37, 99, 235, 0.18),
+ 0 2px 6px rgba(15, 38, 73, 0.04);
+ --shadow-lg: 0 24px 60px -20px rgba(37, 99, 235, 0.25),
+ 0 6px 16px rgba(15, 38, 73, 0.06);
+
+ --radius-sm: 8px;
+ --radius-md: 14px;
+ --radius-lg: 22px;
+
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
+ Helvetica, Arial, sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo,
+ Consolas, monospace;
+
+ --max-w: 1120px;
+ --max-w-narrow: 760px;
+}
+
+* { box-sizing: border-box; }
+
+html { -webkit-text-size-adjust: 100%; }
+
+body {
+ margin: 0;
+ font-family: var(--font-sans);
+ font-size: 17px;
+ line-height: 1.6;
+ color: var(--c-ink);
+ background: var(--c-bg);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ background-image:
+ radial-gradient(ellipse 80% 50% at 50% -10%, var(--c-blue-tint), transparent 70%),
+ radial-gradient(ellipse 60% 40% at 90% 10%, #f0f7ff, transparent 70%);
+ background-repeat: no-repeat;
+}
+
+a {
+ color: var(--c-blue-deep);
+ text-decoration: none;
+ transition: color 160ms ease;
+}
+a:hover { color: var(--c-blue); }
+
+img, svg { display: block; max-width: 100%; }
+
+/* ---------- layout helpers ---------- */
+
+.container {
+ width: 100%;
+ max-width: var(--max-w);
+ margin: 0 auto;
+ padding: 0 24px;
+}
+
+.container.narrow { max-width: var(--max-w-narrow); }
+
+.eyebrow {
+ display: inline-block;
+ font-size: 13px;
+ font-weight: 500;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--c-blue-deep);
+ background: var(--c-blue-tint);
+ padding: 6px 12px;
+ border-radius: 999px;
+ border: 1px solid var(--c-blue-soft);
+}
+.eyebrow.center { display: inline-block; }
+
+.center { text-align: center; }
+.center.eyebrow { display: inline-block; }
+
+.section {
+ padding: 120px 0;
+}
+
+.section-soft {
+ background: var(--c-bg-soft);
+ border-top: 1px solid var(--c-border-soft);
+ border-bottom: 1px solid var(--c-border-soft);
+}
+
+.section-title {
+ font-size: clamp(28px, 4vw, 44px);
+ line-height: 1.15;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ margin: 18px 0 18px;
+ color: var(--c-ink);
+}
+.section-title.center { text-align: center; max-width: 720px; margin-left: auto; margin-right: auto; }
+
+.section-lede {
+ font-size: 19px;
+ color: var(--c-ink-soft);
+ max-width: 640px;
+ margin: 0 auto 60px;
+ line-height: 1.6;
+}
+.section-lede.center { text-align: center; }
+
+/* center the eyebrow when wrapped */
+.section .eyebrow.center,
+.container .eyebrow.center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: max-content;
+}
+
+/* ---------- header ---------- */
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ background: rgba(255, 255, 255, 0.78);
+ backdrop-filter: saturate(180%) blur(14px);
+ -webkit-backdrop-filter: saturate(180%) blur(14px);
+ border-bottom: 1px solid var(--c-border-soft);
+}
+
+.header-inner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 72px;
+}
+
+.brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 600;
+ font-size: 17px;
+ color: var(--c-ink);
+ letter-spacing: -0.01em;
+}
+.brand:hover { color: var(--c-blue-deep); }
+
+.brand-mark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px; height: 32px;
+ color: var(--c-blue-deep);
+ background: var(--c-blue-tint);
+ border-radius: 9px;
+ border: 1px solid var(--c-blue-soft);
+}
+.brand-mark svg { width: 20px; height: 20px; }
+
+.site-nav {
+ display: flex;
+ align-items: center;
+ gap: 32px;
+}
+.site-nav a {
+ color: var(--c-ink-soft);
+ font-size: 15px;
+ font-weight: 500;
+}
+.site-nav a:hover { color: var(--c-blue-deep); }
+
+.nav-cta {
+ background: var(--c-ink);
+ color: #fff !important;
+ padding: 9px 16px;
+ border-radius: 999px;
+ transition: transform 160ms ease, background 160ms ease;
+}
+.nav-cta:hover {
+ background: var(--c-blue-deep);
+ transform: translateY(-1px);
+}
+
+/* ---------- buttons ---------- */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-family: inherit;
+ font-size: 16px;
+ font-weight: 500;
+ padding: 14px 24px;
+ border-radius: 999px;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition: all 180ms ease;
+ letter-spacing: -0.005em;
+}
+
+.btn-primary {
+ background: var(--c-blue-deep);
+ color: #fff;
+ box-shadow: 0 6px 18px -6px rgba(37, 99, 235, 0.5);
+}
+.btn-primary:hover {
+ background: #1d4ed8;
+ color: #fff;
+ transform: translateY(-1px);
+ box-shadow: 0 10px 24px -8px rgba(37, 99, 235, 0.55);
+}
+
+.btn-ghost {
+ background: transparent;
+ color: var(--c-ink);
+ border-color: var(--c-border);
+}
+.btn-ghost:hover {
+ background: #fff;
+ border-color: var(--c-blue-soft);
+ color: var(--c-blue-deep);
+}
+
+.btn-lg {
+ padding: 16px 30px;
+ font-size: 16px;
+}
+
+/* ---------- hero ---------- */
+
+.hero {
+ padding: 120px 0 80px;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-inner {
+ text-align: center;
+ position: relative;
+}
+
+.hero .eyebrow {
+ margin-bottom: 28px;
+}
+
+.hero-title {
+ font-size: clamp(40px, 6vw, 76px);
+ line-height: 1.05;
+ font-weight: 600;
+ letter-spacing: -0.035em;
+ margin: 0 auto 24px;
+ max-width: 12ch;
+ color: var(--c-ink);
+}
+
+.hero-title .accent {
+ background: linear-gradient(120deg, var(--c-blue-deep), var(--c-accent));
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+.hero-sub {
+ font-size: 20px;
+ line-height: 1.55;
+ color: var(--c-ink-soft);
+ max-width: 600px;
+ margin: 0 auto 40px;
+}
+
+.hero-cta {
+ display: flex;
+ gap: 14px;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-bottom: 80px;
+}
+
+/* ---------- code card ---------- */
+
+.hero-visual {
+ display: flex;
+ justify-content: center;
+ margin-top: 16px;
+}
+
+.code-card {
+ width: 100%;
+ max-width: 640px;
+ background: #ffffff;
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ text-align: left;
+ position: relative;
+}
+
+.code-card::before {
+ content: "";
+ position: absolute;
+ inset: -1px;
+ border-radius: inherit;
+ padding: 1px;
+ background: linear-gradient(160deg, rgba(96,165,250,0.4), rgba(255,255,255,0) 60%);
+ -webkit-mask:
+ linear-gradient(#000 0 0) content-box,
+ linear-gradient(#000 0 0);
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ pointer-events: none;
+}
+
+.code-card-head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 14px 18px;
+ background: var(--c-bg-soft);
+ border-bottom: 1px solid var(--c-border-soft);
+}
+
+.code-card-head .dot {
+ width: 10px; height: 10px;
+ border-radius: 50%;
+ background: #d6dde7;
+}
+.dot-a { background: #ffd1d1; }
+.dot-b { background: #ffe5b4; }
+.dot-c { background: #c8e6c9; }
+
+.code-card-title {
+ margin-left: 12px;
+ font-family: var(--font-mono);
+ font-size: 13px;
+ color: var(--c-ink-mute);
+}
+
+.code-card-body {
+ margin: 0;
+ padding: 22px 24px;
+ font-family: var(--font-mono);
+ font-size: 13.5px;
+ line-height: 1.65;
+ color: var(--c-ink-soft);
+ background: #ffffff;
+ overflow-x: auto;
+ white-space: pre;
+}
+.code-card-body code { font-family: inherit; }
+
+.tk-k { color: #2563eb; }
+.tk-s { color: #0f766e; }
+.tk-n { color: #b45309; }
+.tk-p { color: #94a3b8; }
+
+/* ---------- format grid ---------- */
+
+.format-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.format-card {
+ background: #fff;
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius-md);
+ padding: 32px 26px;
+ box-shadow: var(--shadow-sm);
+ transition: transform 200ms ease, box-shadow 200ms ease, border-color 200ms ease;
+}
+
+.format-card:hover {
+ transform: translateY(-3px);
+ border-color: var(--c-blue-soft);
+ box-shadow: var(--shadow-md);
+}
+
+.format-icon {
+ display: inline-flex;
+ width: 44px;
+ height: 44px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ background: var(--c-blue-tint);
+ color: var(--c-blue-deep);
+ margin-bottom: 18px;
+}
+.format-icon svg { width: 22px; height: 22px; }
+
+.format-card h3 {
+ margin: 0 0 8px;
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--c-ink);
+}
+
+.format-card p {
+ margin: 0;
+ font-size: 15px;
+ line-height: 1.55;
+ color: var(--c-ink-mute);
+}
+
+/* ---------- audience grid ---------- */
+
+.audience-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 24px;
+ margin-top: 20px;
+}
+
+.audience-card {
+ position: relative;
+ background: #fff;
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius-lg);
+ padding: 40px 32px;
+ box-shadow: var(--shadow-sm);
+ transition: transform 200ms ease, box-shadow 200ms ease;
+}
+
+.audience-card:hover {
+ transform: translateY(-3px);
+ box-shadow: var(--shadow-md);
+}
+
+.audience-card-feature {
+ background: linear-gradient(180deg, #ffffff 0%, var(--c-blue-tint) 130%);
+ border-color: var(--c-blue-soft);
+}
+
+.audience-num {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--c-blue-deep);
+ letter-spacing: 0.04em;
+ margin-bottom: 16px;
+}
+
+.audience-card h3 {
+ margin: 0 0 12px;
+ font-size: 22px;
+ font-weight: 600;
+ letter-spacing: -0.015em;
+ color: var(--c-ink);
+}
+
+.audience-card p {
+ margin: 0;
+ color: var(--c-ink-soft);
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+/* ---------- value list ---------- */
+
+.value-list {
+ list-style: none;
+ padding: 0;
+ margin: 40px 0 0;
+ display: grid;
+ gap: 22px;
+}
+
+.value-list li {
+ display: flex;
+ align-items: flex-start;
+ gap: 18px;
+ background: #fff;
+ border: 1px solid var(--c-border);
+ border-radius: var(--radius-md);
+ padding: 22px 26px;
+ box-shadow: var(--shadow-sm);
+}
+
+.value-check {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ flex: 0 0 36px;
+ border-radius: 50%;
+ background: var(--c-blue-tint);
+ color: var(--c-blue-deep);
+}
+.value-check svg { width: 18px; height: 18px; }
+
+.value-list strong {
+ display: block;
+ font-weight: 600;
+ font-size: 17px;
+ color: var(--c-ink);
+ margin-bottom: 4px;
+}
+
+.value-list p {
+ margin: 0;
+ color: var(--c-ink-mute);
+ font-size: 15.5px;
+ line-height: 1.55;
+}
+
+/* ---------- CTA card ---------- */
+
+.cta-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: linear-gradient(160deg, #ffffff 0%, var(--c-blue-tint) 100%);
+ border: 1px solid var(--c-blue-soft);
+ border-radius: var(--radius-lg);
+ padding: 72px 40px;
+ box-shadow: var(--shadow-md);
+ overflow: hidden;
+}
+
+.cta-glow {
+ position: absolute;
+ top: -120px;
+ left: 50%;
+ width: 360px;
+ height: 360px;
+ transform: translateX(-50%);
+ background: radial-gradient(closest-side, rgba(96, 165, 250, 0.35), rgba(96, 165, 250, 0) 70%);
+ pointer-events: none;
+ z-index: 0;
+}
+
+.cta-title {
+ position: relative;
+ z-index: 1;
+ margin: 0 0 28px;
+ font-size: clamp(26px, 3.4vw, 36px);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--c-ink);
+ text-align: center;
+}
+
+.cta-btn {
+ position: relative;
+ z-index: 1;
+ gap: 10px;
+}
+
+.cta-btn svg {
+ width: 18px;
+ height: 18px;
+}
+
+/* ---------- footer ---------- */
+
+.site-footer {
+ border-top: 1px solid var(--c-border-soft);
+ background: var(--c-bg);
+ padding: 60px 0 50px;
+}
+
+.footer-inner {
+ text-align: center;
+}
+
+.footer-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ font-weight: 600;
+ color: var(--c-ink);
+ margin-bottom: 14px;
+}
+.footer-brand .brand-mark { color: var(--c-blue-deep); }
+.footer-brand .brand-mark svg { width: 20px; height: 20px; }
+
+.footer-tagline {
+ margin: 0 0 12px;
+ color: var(--c-ink-soft);
+ font-size: 15px;
+}
+
+.footer-meta {
+ margin: 0;
+ color: var(--c-ink-mute);
+ font-size: 13px;
+}
+
+/* ---------- responsive ---------- */
+
+@media (max-width: 880px) {
+ .format-grid { grid-template-columns: repeat(2, 1fr); }
+ .audience-grid { grid-template-columns: 1fr; }
+ .section { padding: 90px 0; }
+ .hero { padding: 80px 0 40px; }
+}
+
+@media (max-width: 640px) {
+ body { font-size: 16px; }
+ .site-nav { gap: 18px; }
+ .site-nav a:not(.nav-cta) { display: none; }
+ .header-inner { height: 64px; }
+ .hero-cta { flex-direction: column; align-items: stretch; }
+ .hero-cta .btn { width: 100%; }
+ .code-card-body { font-size: 12px; padding: 18px; }
+ .format-grid { grid-template-columns: 1fr; }
+ .cta-card { padding: 40px 24px; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ * { transition: none !important; }
+}
diff --git a/landing/static/favicon.svg b/landing/static/favicon.svg
new file mode 100644
index 0000000..256ac13
--- /dev/null
+++ b/landing/static/favicon.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+ <rect width="32" height="32" rx="7" fill="#eef4ff"/>
+ <rect x="6" y="8" width="20" height="3" rx="1.5" fill="#2563eb"/>
+ <rect x="6" y="15" width="15" height="3" rx="1.5" fill="#2563eb" opacity="0.7"/>
+ <rect x="6" y="22" width="10" height="3" rx="1.5" fill="#2563eb" opacity="0.4"/>
+</svg>
diff --git a/landing/static/robots.txt b/landing/static/robots.txt
new file mode 100644
index 0000000..f0d8319
--- /dev/null
+++ b/landing/static/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://tidyindex.com/sitemap.xml