authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 19:14:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 19:14:12-07:00
log232de5e8aac61e6f3de4bc61d10746167db9e905
treeac8c176d6cf84d56db414d2db9b4e20c1a428cf0
parent617edab6539ee6573055dc9fc45fdeacdb12f673
signature Signed by PGP key B5690EEEBB952194

feat(marketing): fresh screenshot and floating marks on the homepage (#9547)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

6 files changed, 147 insertions(+), 160 deletions(-)

apps/marketing/public/app-desktop.webp created
Binary files /dev/null and b/apps/marketing/public/app-desktop.webp differ
apps/marketing/public/harnesses/antigravity.png created
Binary files /dev/null and b/apps/marketing/public/harnesses/antigravity.png differ
apps/marketing/public/updated-screenshot.webp deleted
Binary files a/apps/marketing/public/updated-screenshot.webp and /dev/null differ
apps/marketing/src/layouts/Layout.astro+10-14
......@@ -14,7 +14,7 @@ interface Props {
1414
1515const {
1616 title = "T3 Code",
17 description = "T3 Code — The open-source control plane for coding agents.",
17 description = "T3 Code. The open-source control plane for coding agents.",
1818 pageClass,
1919} = Astro.props;
2020---
......@@ -268,21 +268,17 @@ const {
268268 }
269269 }
270270
271 @keyframes pulse {
272 50% { opacity: 0.4; }
271 /* Page-load sequence: [data-rise] plays once with delay --d. */
272 @keyframes rise {
273 from { opacity: 0; transform: translateY(16px); }
274 to { opacity: 1; transform: none; }
273275 }
274
275 @keyframes spin {
276 to { transform: rotate(360deg); }
277 }
278
279 @keyframes floatDrift {
280 0%, 100% { translate: 0 0; }
281 50% { translate: 0 -10px; }
276 [data-rise] {
277 animation: rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
278 animation-delay: var(--d, 0ms);
282279 }
283
284 @keyframes blink {
285 50% { opacity: 0; }
280 @media (prefers-reduced-motion: reduce) {
281 [data-rise] { animation: none; }
286282 }
287283</style>
288284
apps/marketing/src/lib/site.ts+2-2
......@@ -7,6 +7,6 @@ export const ANDROID_PLAY_STORE_URL =
77 "https://play.google.com/store/apps/details?id=com.t3tools.t3code";
88
99export const MARKETING_STATS = {
10 githubStars: "14k+",
11 users: "100,000",
10 githubStars: "21k+",
11 users: "200,000",
1212} as const;
apps/marketing/src/pages/index.astro+135-144
......@@ -26,34 +26,31 @@ const mobileEndorsementRows = [
2626 <div class="hero-grid" aria-hidden="true"></div>
2727
2828 <div class="hero-float" aria-hidden="true">
29 <span class="hero-float-mark hf-claude" title="Claude Code">
30 <img src="/harnesses/claude-ai-icon.svg" alt="" />
31 </span>
32 <span class="hero-float-mark hf-codex" title="Codex">
33 <img src="/harnesses/openai_dark.svg" alt="" />
34 </span>
35 <span class="hero-float-mark hf-opencode" title="OpenCode">
36 <img src="/harnesses/opencode-dark.svg" alt="" />
37 </span>
38 <span class="hero-float-mark hf-cursor" title="Cursor">
39 <img src="/harnesses/cursor_light.svg" alt="" />
40 </span>
41 <span class="hero-float-mark hf-grok" title="Grok">
42 <img src="/harnesses/grok-dark.svg" alt="" />
43 </span>
29 {[
30 { key: "opencode", icon: "/harnesses/opencode-dark.svg", cls: "hf-opencode" },
31 { key: "antigravity", icon: "/harnesses/antigravity.png", cls: "hf-antigravity" },
32 { key: "claude", icon: "/harnesses/claude-ai-icon.svg", cls: "hf-claude" },
33 { key: "codex", icon: "/harnesses/openai_dark.svg", cls: "hf-codex" },
34 { key: "grok", icon: "/harnesses/grok-dark.svg", cls: "hf-grok" },
35 { key: "cursor", icon: "/harnesses/cursor_light.svg", cls: "hf-cursor" },
36 ].map((m) => (
37 <span class:list={["hero-float-mark", m.cls]}>
38 <span class="hero-float-card"><img src={m.icon} alt="" /></span>
39 </span>
40 ))}
4441 </div>
4542
4643 <div class="container hero-inner">
4744 <h1 class="hero-title display">
48 The open-source<br />control plane<br />for coding agents.
45 <span data-rise>The open-source</span><br /><span data-rise style="--d:90ms">control plane</span><br /><span data-rise style="--d:180ms">for coding agents.</span>
4946 </h1>
5047
51 <p class="hero-sub">
52 Orchestrate Claude Code, Codex, OpenCode, Cursor, and Grok from one surface.
48 <p class="hero-sub" data-rise style="--d:300ms">
49 Orchestrate Claude Code, Codex, Antigravity, OpenCode, Cursor, and Grok from one surface.
5350 Bring your own subscription. Fork the whole thing.
5451 </p>
5552
56 <div class="hero-actions">
53 <div class="hero-actions" data-rise style="--d:400ms">
5754 <a
5855 id="download-btn"
5956 href="https://github.com/pingdotgg/t3code/releases"
......@@ -79,17 +76,20 @@ const mobileEndorsementRows = [
7976 </p>
8077 </div>
8178
82 <div class="hero-preview">
83 <figure class="hero-screenshot-frame">
84 <img
85 src="/updated-screenshot.webp"
86 alt="T3 Code app showing multiple coding agent threads in one workspace"
87 width="2508"
88 height="1682"
89 loading="eager"
90 decoding="async"
91 />
92 </figure>
79 <!-- The rise wrapper is separate so its animated transform does not override the preview tilt. -->
80 <div data-rise style="--d:520ms">
81 <div class="hero-preview">
82 <figure class="hero-screenshot-frame">
83 <img
84 src="/app-desktop.webp"
85 alt="T3 Code with a settled thread beside its pull request diff"
86 width="2880"
87 height="1800"
88 loading="eager"
89 decoding="async"
90 />
91 </figure>
92 </div>
9393 </div>
9494 </div>
9595 </section>
......@@ -186,9 +186,9 @@ const mobileEndorsementRows = [
186186 <div class="section-head">
187187 <h2>Bring your own sub</h2>
188188 <p>
189 T3 Code doesn't resell tokens. Plug in Claude Code, Codex, OpenCode,
190 Cursor, or Grok with the credentials you already have — we orchestrate
191 them, you keep your plan.
189 T3 Code doesn't resell tokens. Plug in Claude Code, Codex, Antigravity,
190 OpenCode, Cursor, or Grok with the credentials you already have. We
191 orchestrate them, you keep your plan.
192192 </p>
193193 </div>
194194
......@@ -228,6 +228,13 @@ const mobileEndorsementRows = [
228228 <div class="harness-tag">grok login</div>
229229 </div>
230230 </div>
231 <div class="harness">
232 <div class="harness-mark"><img src="/harnesses/antigravity.png" alt="" /></div>
233 <div class="harness-meta">
234 <div class="harness-name">Antigravity</div>
235 <div class="harness-tag">Google sign-in</div>
236 </div>
237 </div>
231238 </div>
232239
233240 <div class="harness-footer">
......@@ -455,6 +462,28 @@ const mobileEndorsementRows = [
455462 }
456463
457464 init();
465
466 // Marks lean a few pixels toward the pointer, each by its own depth.
467 const hero = document.querySelector<HTMLElement>(".hero");
468 const field = document.querySelector<HTMLElement>(".hero-float");
469 const fine = window.matchMedia("(pointer: fine) and (prefers-reduced-motion: no-preference)").matches;
470 if (hero && field && fine) {
471 let raf = 0;
472 let nx = 0;
473 let ny = 0;
474 const apply = () => {
475 raf = 0;
476 field.style.setProperty("--px", `${(nx * 18).toFixed(1)}px`);
477 field.style.setProperty("--py", `${(ny * 14).toFixed(1)}px`);
478 };
479 hero.addEventListener("pointermove", (e) => {
480 const r = hero.getBoundingClientRect();
481 nx = ((e.clientX - r.left) / r.width - 0.5) * 2;
482 ny = ((e.clientY - r.top) / r.height - 0.5) * 2;
483 if (!raf) raf = requestAnimationFrame(apply);
484 });
485 hero.addEventListener("pointerleave", () => { nx = 0; ny = 0; if (!raf) raf = requestAnimationFrame(apply); });
486 }
458487</script>
459488
460489<style>
......@@ -541,6 +570,8 @@ const mobileEndorsementRows = [
541570 max-width: 20ch;
542571 text-wrap: balance;
543572 }
573 /* Inline boxes ignore transforms, so each rising line needs its own block. */
574 .hero-title span { display: inline-block; }
544575
545576 .hero-sub {
546577 color: var(--fg-muted);
......@@ -624,52 +655,83 @@ const mobileEndorsementRows = [
624655 :global([data-platform="linux"]) .dl-icon--linux { display: block; }
625656 :global(html:not([data-platform])) .dl-icon--apple { display: block; }
626657
627 /* Floating harness marks */
658 /* Floating harness marks. Six points on an ellipse around the hero copy:
659 two small at the top, two high and wide, two low and close. Symmetric,
660 and none of them touch the screenshot. Each mark flies in from its own
661 side on load, then drifts on its own period, and the whole set leans a
662 few pixels toward the pointer. */
628663 .hero-float {
629664 position: absolute;
630665 inset: 0;
631666 pointer-events: none;
632667 z-index: 1;
668 --px: 0px;
669 --py: 0px;
633670 }
634671
635672 .hero-float-mark {
636673 position: absolute;
637 width: 96px;
638 height: 96px;
639 border-radius: 24px;
674 left: calc(50% + var(--x));
675 top: var(--y);
676 width: var(--s);
677 height: var(--s);
678 margin-left: calc(var(--s) / -2);
679 translate: calc(var(--px) * var(--depth)) calc(var(--py) * var(--depth));
680 transition: translate 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
681 }
682
683 .hero-float-card {
640684 display: grid;
641685 place-items: center;
642 background: rgba(20, 20, 24, 0.9);
686 width: 100%;
687 height: 100%;
688 border-radius: 25%;
689 background: radial-gradient(circle at 30% 25%, var(--tint, rgba(255, 255, 255, 0.08)), rgba(20, 20, 24, 0.92) 65%);
643690 border: 1px solid var(--border);
644 backdrop-filter: blur(10px);
645 -webkit-backdrop-filter: blur(10px);
646691 box-shadow:
647692 0 20px 48px -16px rgba(0, 0, 0, 0.65),
648693 0 2px 0 0 rgba(255, 255, 255, 0.04) inset;
649 animation: floatDrift 9s ease-in-out infinite;
694 animation:
695 mark-in 1s cubic-bezier(0.2, 0.7, 0.2, 1) var(--d) both,
696 mark-drift var(--dur) ease-in-out calc(var(--d) + 1s) infinite;
650697 }
651698
652 .hero-float-mark img {
653 width: 54px;
654 height: 54px;
699 .hero-float-card img {
700 width: 56%;
701 height: 56%;
655702 display: block;
656703 object-fit: contain;
657704 }
658705
659 .hero-float-mark.hf-claude { background: radial-gradient(circle at 30% 25%, rgba(217, 119, 87, 0.22), rgba(20, 20, 24, 0.92) 65%); top: 8%; left: 7%; animation-delay: 0s; transform: rotate(-8deg); }
660 .hero-float-mark.hf-codex { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 6%; right: 7%; animation-delay: -2.5s; transform: rotate(6deg); }
661 .hero-float-mark.hf-opencode { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.06), rgba(20, 20, 24, 0.92) 65%); top: 30%; left: 5%; animation-delay: -5s; transform: rotate(4deg); }
662 .hero-float-mark.hf-cursor { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 30%; right: 5%; animation-delay: -7s; transform: rotate(-5deg); }
663 .hero-float-mark.hf-grok { background: radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.08), rgba(20, 20, 24, 0.92) 65%); top: 52%; left: 6%; animation-delay: -1.2s; transform: rotate(3deg); }
706 /* The Antigravity icon ships with its own rounded dark tile, so it fills the
707 card edge to edge instead of sitting inside it. */
708 .hf-antigravity .hero-float-card { background: #0d0d10; border-color: rgba(255, 255, 255, 0.1); }
709 .hf-antigravity .hero-float-card img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
710
711 @keyframes mark-in {
712 from { opacity: 0; transform: translate(var(--fx), var(--fy)) rotate(calc(var(--rot) + 24deg)) scale(0.6); }
713 to { opacity: 1; transform: translate(0, 0) rotate(var(--rot)) scale(1); }
714 }
715 @keyframes mark-drift {
716 0%, 100% { transform: translate(0, 0) rotate(var(--rot)); }
717 50% { transform: translate(var(--dx), var(--dy)) rotate(calc(var(--rot) + var(--wob))); }
718 }
719
720 .hf-opencode { --x: max(-300px, -19vw); --y: 20px; --s: 64px; --rot: 6deg; --depth: 0.6; --d: 620ms; --dur: 8.5s; --dx: 2px; --dy: -8px; --wob: 3deg; --fx: -40px; --fy: -80px; }
721 .hf-antigravity { --x: min(300px, 19vw); --y: 20px; --s: 64px; --rot: -5deg; --depth: 0.6; --d: 580ms; --dur: 9.8s; --dx: -2px; --dy: -9px; --wob: -3deg; --fx: 40px; --fy: -80px; }
722 .hf-claude { --x: max(-560px, -38vw); --y: 128px; --s: 96px; --rot: -8deg; --depth: 1.2; --d: 380ms; --dur: 10.5s; --dx: 4px; --dy: -12px; --wob: -2deg; --fx: -160px; --fy: -40px; --tint: rgba(217, 119, 87, 0.22); }
723 .hf-codex { --x: min(560px, 38vw); --y: 128px; --s: 96px; --rot: 7deg; --depth: 1.2; --d: 460ms; --dur: 9.2s; --dx: -4px; --dy: -10px; --wob: 2deg; --fx: 160px; --fy: -40px; }
724 .hf-grok { --x: max(-440px, -30vw); --y: 440px; --s: 80px; --rot: 4deg; --depth: 0.9; --d: 540ms; --dur: 11.5s; --dx: 3px; --dy: -9px; --wob: -3deg; --fx: -120px; --fy: 80px; }
725 .hf-cursor { --x: min(440px, 30vw); --y: 440px; --s: 80px; --rot: -5deg; --depth: 0.9; --d: 700ms; --dur: 7.8s; --dx: -3px; --dy: -11px; --wob: 3deg; --fx: 120px; --fy: 80px; }
726
727 @media (prefers-reduced-motion: reduce) {
728 .hero-float-card { animation: none; transform: rotate(var(--rot)); }
729 .hero-float-mark { transition: none; }
730 }
664731
665732 @media (max-width: 1180px) {
666 .hero-float-mark { width: 76px; height: 76px; border-radius: 20px; }
667 .hero-float-mark img { width: 42px; height: 42px; }
668 .hero-float-mark.hf-claude { top: 4%; left: 3%; }
669 .hero-float-mark.hf-codex { top: 2%; right: 3%; }
670 .hero-float-mark.hf-opencode { top: 26%; left: 2%; }
671 .hero-float-mark.hf-cursor { top: 24%; right: 2%; }
672 .hero-float-mark.hf-grok { top: 48%; left: 2%; transform: rotate(3deg); }
733 .hf-claude, .hf-codex { --s: 80px; }
734 .hf-grok, .hf-cursor { --s: 68px; --y: 420px; }
673735 }
674736
675737 @media (max-width: 820px) {
......@@ -686,90 +748,18 @@ const mobileEndorsementRows = [
686748 margin-bottom: 96px;
687749 }
688750
689 .hero-float {
690 position: absolute;
691 inset: 0;
692 display: block;
693 }
694
695 .hero-float-mark {
696 position: absolute;
697 width: 78px;
698 height: 78px;
699 border-radius: 20px;
700 }
701
702 .hero-float-mark img {
703 width: 44px;
704 height: 44px;
705 }
706
707 /* Three above the headline, two beside the CTA — keeps the copy clear. */
708 .hero-float-mark.hf-claude {
709 top: 44px;
710 left: 10px;
711 transform: rotate(-10deg);
712 }
713
714 .hero-float-mark.hf-grok {
715 top: 44px;
716 left: calc(50% - 39px);
717 right: auto;
718 transform: rotate(-4deg);
719 }
720
721 .hero-float-mark.hf-opencode {
722 top: 474px;
723 left: 4px;
724 right: auto;
725 transform: rotate(-6deg);
726 }
727
728 .hero-float-mark.hf-codex {
729 top: 44px;
730 right: 10px;
731 transform: rotate(8deg);
732 }
733
734 .hero-float-mark.hf-cursor {
735 top: 474px;
736 right: 4px;
737 left: auto;
738 transform: rotate(7deg);
739 }
751 /* Four above the headline, two beside the CTA. Keeps the copy clear. */
752 .hf-claude { --x: -37vw; --y: 44px; --s: 64px; --rot: -10deg; }
753 .hf-opencode { --x: -12vw; --y: 52px; --s: 56px; --rot: -4deg; }
754 .hf-antigravity { --x: 12vw; --y: 52px; --s: 56px; --rot: 5deg; }
755 .hf-codex { --x: 37vw; --y: 44px; --s: 64px; --rot: 8deg; }
756 .hf-grok { --x: -36vw; --y: 474px; --s: 64px; --rot: -6deg; }
757 .hf-cursor { --x: 36vw; --y: 474px; --s: 64px; --rot: 7deg; }
740758 }
741759
742760 @media (max-width: 340px) {
743 .hero-float-mark.hf-grok {
744 top: 57px;
745 left: calc(50% - 26px);
746 width: 52px;
747 height: 52px;
748 border-radius: 14px;
749 }
750
751 .hero-float-mark.hf-opencode,
752 .hero-float-mark.hf-cursor {
753 top: 580px;
754 width: 52px;
755 height: 52px;
756 border-radius: 14px;
757 }
758
759 .hero-float-mark.hf-opencode {
760 left: 0;
761 }
762
763 .hero-float-mark.hf-cursor {
764 right: 0;
765 }
766
767 .hero-float-mark.hf-grok img,
768 .hero-float-mark.hf-opencode img,
769 .hero-float-mark.hf-cursor img {
770 width: 30px;
771 height: 30px;
772 }
761 .hf-opencode, .hf-antigravity, .hf-grok, .hf-cursor { --s: 48px; }
762 .hf-grok, .hf-cursor { --y: 580px; }
773763 }
774764
775765 .hero-preview {
......@@ -786,7 +776,7 @@ const mobileEndorsementRows = [
786776 .hero-screenshot-frame {
787777 position: relative;
788778 overflow: hidden;
789 aspect-ratio: 2508 / 1682;
779 aspect-ratio: 2880 / 1800;
790780 border-radius: 14px;
791781 background: #0a0a0c;
792782 box-shadow: 0 28px 64px -28px rgba(0, 0, 0, 0.75);
......@@ -811,7 +801,7 @@ const mobileEndorsementRows = [
811801
812802 .harness-grid {
813803 display: grid;
814 grid-template-columns: repeat(5, 1fr);
804 grid-template-columns: repeat(6, 1fr);
815805 gap: 0;
816806 border: 1px solid var(--border);
817807 border-radius: var(--radius);
......@@ -833,7 +823,7 @@ const mobileEndorsementRows = [
833823 flex-shrink: 0; width: 28px; height: 28px;
834824 display: grid; place-items: center;
835825 }
836 .harness-mark img { width: 22px; height: 22px; object-fit: contain; }
826 .harness-mark img { width: 22px; height: 22px; object-fit: contain; border-radius: 5px; }
837827
838828 .harness-meta { flex: 1; min-width: 0; }
839829 .harness-name {
......@@ -1244,6 +1234,10 @@ const mobileEndorsementRows = [
12441234 margin-bottom: 24px;
12451235 }
12461236
1237 @keyframes blink {
1238 50% { opacity: 0; }
1239 }
1240
12471241 @keyframes endorsementMarquee {
12481242 from {
12491243 transform: translateX(0);
......@@ -1266,10 +1260,7 @@ const mobileEndorsementRows = [
12661260 .harness-grid { grid-template-columns: 1fr 1fr; }
12671261 .harness { border-right: 0; border-bottom: 1px solid var(--border); }
12681262 .harness:nth-child(odd) { border-right: 1px solid var(--border); }
1269 .harness:nth-child(5):last-child {
1270 grid-column: 1 / -1;
1271 border-right: 0;
1272 }
1263 .harness:nth-child(even) { border-right: 0; }
12731264 .git-inner { grid-template-columns: 1fr; gap: 40px; }
12741265 .open-grid { grid-template-columns: 1fr; }
12751266 }