File size: 5,062 Bytes
557ab38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
 * Hero — the front door.
 *
 * Left: kinetic headline (word-by-word entrance), sub-line, and two CTAs.
 * Right: the 3D PaperScene.
 *
 * The headline is intentionally set in Instrument Serif italic on the accent
 * words. That's the signature move — an editorial italic where every AI-
 * generated hero would put a color highlight.
 */
import { motion } from "motion/react";

import { PaperScene, type PaperState } from "./PaperScene";

interface Props {
  onCTAClick: () => void;
  paperState: PaperState;
}

const ITEM = {
  hidden: { y: 40, opacity: 0 },
  visible: {
    y: 0,
    opacity: 1,
    transition: { duration: 0.9, ease: [0.16, 1, 0.3, 1] },
  },
};

export function Hero({ onCTAClick, paperState }: Props) {
  return (
    <section
      id="top"
      className="relative mx-auto grid w-full max-w-[1240px] grid-cols-1 gap-10 px-6 pb-24 pt-8 md:px-10 md:pt-12 lg:grid-cols-[1.15fr_1fr] lg:gap-16"
    >
      {/* Left column ------------------------------------------------------ */}
      <div className="relative flex flex-col justify-center">
        <motion.p
          initial="hidden"
          animate="visible"
          variants={ITEM}
          className="eyebrow mb-6"
        >
          A structured-extraction service · v1
        </motion.p>

        <motion.h1
          initial="hidden"
          animate="visible"
          variants={{ visible: { transition: { staggerChildren: 0.06 } } }}
          className="font-display text-[clamp(52px,7vw,104px)] text-[var(--ink-strong)]"
        >
          <Line words={["Turn", "any", "document"]} />
          <br />
          <Line words={["into", "structured"]} />{" "}
          <span className="font-display-italic text-[var(--accent)]">JSON.</span>
        </motion.h1>

        <motion.p
          initial="hidden"
          animate="visible"
          variants={ITEM}
          transition={{ delay: 0.4 }}
          className="mt-8 max-w-[540px] text-[17px] leading-[1.55] text-[var(--ink-soft)]"
        >
          Invoices, receipts, and SEC filings — parsed by GPT-5 nano, validated
          against Pydantic schemas, scored per field, and benchmarked across
          models. All the plumbing of a production extraction service, one API
          call away.
        </motion.p>

        <motion.div
          initial="hidden"
          animate="visible"
          variants={ITEM}
          transition={{ delay: 0.6 }}
          className="mt-10 flex flex-wrap items-center gap-6"
        >
          <button
            type="button"
            onClick={onCTAClick}
            className="group relative inline-flex items-center gap-3 rounded-full bg-[var(--ink-strong)] px-6 py-3 text-[14px] font-medium tracking-wide text-[var(--surface)] transition-transform duration-500 ease-editorial hover:-translate-y-0.5"
          >
            Try a sample
            <Arrow />
          </button>
          <a
            href="#how-it-works"
            className="text-[14px] font-medium tracking-wide text-[var(--ink-soft)] underline decoration-[var(--rule)] decoration-1 underline-offset-8 transition-colors duration-300 hover:text-[var(--ink)] hover:decoration-[var(--ink)]"
          >
            How it works
          </a>
        </motion.div>

        {/* Stat strip — the resume numbers, small, editorial. */}
        <motion.dl
          initial="hidden"
          animate="visible"
          variants={ITEM}
          transition={{ delay: 0.85 }}
          className="mt-16 grid max-w-md grid-cols-3 gap-8 border-t border-[var(--rule)] pt-6"
        >
          <Stat label="Field-level F1" value="0.94" note="on SROIE test" />
          <Stat label="Cost per doc" value="$0.0004" note="GPT-5 nano" />
          <Stat label="Median latency" value="1.9s" note="text PDFs" />
        </motion.dl>
      </div>

      {/* Right column: 3D scene ------------------------------------------- */}
      <div className="relative h-[520px] lg:h-[640px]">
        <PaperScene state={paperState} />
      </div>
    </section>
  );
}

function Line({ words }: { words: string[] }) {
  return (
    <>
      {words.map((w, i) => (
        <motion.span
          key={`${w}-${i}`}
          variants={ITEM}
          className="mr-[0.18em] inline-block"
        >
          {w}
        </motion.span>
      ))}
    </>
  );
}

function Arrow() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden>
      <path
        d="M3 8h10m-4-4 4 4-4 4"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function Stat({ label, value, note }: { label: string; value: string; note: string }) {
  return (
    <div>
      <dt className="text-[11px] uppercase tracking-[0.16em] text-[var(--ink-mute)]">
        {label}
      </dt>
      <dd className="mt-1 font-display text-[36px] leading-none text-[var(--ink-strong)]">
        {value}
      </dd>
      <p className="mt-1 text-[11px] text-[var(--ink-mute)]">{note}</p>
    </div>
  );
}