| ... | ... | @@ -17,16 +17,6 @@ const screenshot = await getImage({ |
| 17 | 17 | quality: 90, |
| 18 | 18 | }); |
| 19 | 19 | |
| 20 | | const desktopEndorsementRows = [ |
| 21 | | tweets.filter((_, index) => index % 2 === 0), |
| 22 | | tweets.filter((_, index) => index % 2 === 1), |
| 23 | | ]; |
| 24 | | |
| 25 | | const mobileEndorsementRows = [ |
| 26 | | tweets.filter((_, index) => index % 3 === 0), |
| 27 | | tweets.filter((_, index) => index % 3 === 1), |
| 28 | | tweets.filter((_, index) => index % 3 === 2), |
| 29 | | ]; |
| 30 | 20 | --- |
| 31 | 21 | |
| 32 | 22 | <Layout> |
| ... | ... | @@ -121,80 +111,29 @@ const mobileEndorsementRows = [ |
| 121 | 111 | </div> |
| 122 | 112 | </div> |
| 123 | 113 | |
| 124 | | <div class="endorsement-carousel endorsement-carousel--desktop" aria-label="Developer endorsements"> |
| 125 | | { |
| 126 | | desktopEndorsementRows.map((row, rowIndex) => ( |
| 127 | | <div class:list={["endorsement-row", rowIndex === 1 && "is-reverse"]}> |
| 128 | | <div class="endorsement-track"> |
| 129 | | {[0, 1].map((groupIndex) => ( |
| 130 | | <div class="endorsement-track-group" aria-hidden={groupIndex === 1 ? "true" : undefined}> |
| 131 | | {row.map((tweet) => ( |
| 132 | | <a |
| 133 | | class="endorsement-card" |
| 134 | | href={tweet.link} |
| 135 | | target="_blank" |
| 136 | | rel="noopener noreferrer" |
| 137 | | aria-label={`Read ${tweet.handle}'s post on X`} |
| 138 | | tabindex={groupIndex === 1 ? "-1" : undefined} |
| 139 | | > |
| 140 | | <div class="endorsement-author"> |
| 141 | | <img |
| 142 | | src={`/pfps/${tweet.handle}.webp`} |
| 143 | | alt="" |
| 144 | | width="28" |
| 145 | | height="28" |
| 146 | | loading="lazy" |
| 147 | | decoding="async" |
| 148 | | /> |
| 149 | | <div class="endorsement-handle">@{tweet.handle}</div> |
| 150 | | </div> |
| 151 | | <p>{tweet.excerpt ?? tweet.content}</p> |
| 152 | | </a> |
| 153 | | ))} |
| 154 | | </div> |
| 155 | | ))} |
| 156 | | </div> |
| 157 | | </div> |
| 158 | | )) |
| 159 | | } |
| 160 | | </div> |
| 161 | | |
| 162 | | <div class="endorsement-carousel endorsement-carousel--mobile" aria-label="Developer endorsements"> |
| 163 | | { |
| 164 | | mobileEndorsementRows.map((row, rowIndex) => ( |
| 165 | | <div class:list={["endorsement-row", rowIndex === 1 && "is-reverse"]}> |
| 166 | | <div class="endorsement-track"> |
| 167 | | {[0, 1].map((groupIndex) => ( |
| 168 | | <div class="endorsement-track-group" aria-hidden={groupIndex === 1 ? "true" : undefined}> |
| 169 | | {row.map((tweet) => ( |
| 170 | | <a |
| 171 | | class="endorsement-card" |
| 172 | | href={tweet.link} |
| 173 | | target="_blank" |
| 174 | | rel="noopener noreferrer" |
| 175 | | aria-label={`Read ${tweet.handle}'s post on X`} |
| 176 | | tabindex={groupIndex === 1 ? "-1" : undefined} |
| 177 | | > |
| 178 | | <div class="endorsement-author"> |
| 179 | | <img |
| 180 | | src={`/pfps/${tweet.handle}.webp`} |
| 181 | | alt="" |
| 182 | | width="28" |
| 183 | | height="28" |
| 184 | | loading="lazy" |
| 185 | | decoding="async" |
| 186 | | /> |
| 187 | | <div class="endorsement-handle">@{tweet.handle}</div> |
| 188 | | </div> |
| 189 | | <p>{tweet.excerpt ?? tweet.content}</p> |
| 190 | | </a> |
| 191 | | ))} |
| 192 | | </div> |
| 193 | | ))} |
| 194 | | </div> |
| 195 | | </div> |
| 196 | | )) |
| 197 | | } |
| 114 | <div class="endorsement-carousel" role="region" aria-label="Developer endorsements" tabindex="0"> |
| 115 | {tweets.map((tweet) => ( |
| 116 | <a |
| 117 | class="endorsement-card" |
| 118 | href={tweet.link} |
| 119 | target="_blank" |
| 120 | rel="noopener noreferrer" |
| 121 | aria-label={`Read ${tweet.handle}'s post on X`} |
| 122 | > |
| 123 | <div class="endorsement-author"> |
| 124 | <img |
| 125 | src={`/pfps/${tweet.handle}.webp`} |
| 126 | alt="" |
| 127 | width="28" |
| 128 | height="28" |
| 129 | loading="lazy" |
| 130 | decoding="async" |
| 131 | /> |
| 132 | <div class="endorsement-handle">@{tweet.handle}</div> |
| 133 | </div> |
| 134 | <p>{tweet.excerpt ?? tweet.content}</p> |
| 135 | </a> |
| 136 | ))} |
| 198 | 137 | </div> |
| 199 | 138 | </section> |
| 200 | 139 | |
| ... | ... | @@ -480,28 +419,6 @@ const mobileEndorsementRows = [ |
| 480 | 419 | } |
| 481 | 420 | |
| 482 | 421 | init(); |
| 483 | | |
| 484 | | // Marks lean a few pixels toward the pointer, each by its own depth. |
| 485 | | const hero = document.querySelector<HTMLElement>(".hero"); |
| 486 | | const field = document.querySelector<HTMLElement>(".hero-float"); |
| 487 | | const fine = window.matchMedia("(pointer: fine) and (prefers-reduced-motion: no-preference)").matches; |
| 488 | | if (hero && field && fine) { |
| 489 | | let raf = 0; |
| 490 | | let nx = 0; |
| 491 | | let ny = 0; |
| 492 | | const apply = () => { |
| 493 | | raf = 0; |
| 494 | | field.style.setProperty("--px", `${(nx * 18).toFixed(1)}px`); |
| 495 | | field.style.setProperty("--py", `${(ny * 14).toFixed(1)}px`); |
| 496 | | }; |
| 497 | | hero.addEventListener("pointermove", (e) => { |
| 498 | | const r = hero.getBoundingClientRect(); |
| 499 | | nx = ((e.clientX - r.left) / r.width - 0.5) * 2; |
| 500 | | ny = ((e.clientY - r.top) / r.height - 0.5) * 2; |
| 501 | | if (!raf) raf = requestAnimationFrame(apply); |
| 502 | | }); |
| 503 | | hero.addEventListener("pointerleave", () => { nx = 0; ny = 0; if (!raf) raf = requestAnimationFrame(apply); }); |
| 504 | | } |
| 505 | 422 | </script> |
| 506 | 423 | |
| 507 | 424 | <style> |
| ... | ... | @@ -673,18 +590,12 @@ const mobileEndorsementRows = [ |
| 673 | 590 | :global([data-platform="linux"]) .dl-icon--linux { display: block; } |
| 674 | 591 | :global(html:not([data-platform])) .dl-icon--apple { display: block; } |
| 675 | 592 | |
| 676 | | /* Floating harness marks. Six points on an ellipse around the hero copy: |
| 677 | | two small at the top, two high and wide, two low and close. Symmetric, |
| 678 | | and none of them touch the screenshot. Each mark flies in from its own |
| 679 | | side on load, then drifts on its own period, and the whole set leans a |
| 680 | | few pixels toward the pointer. */ |
| 593 | /* Harness marks stay in their final positions after the entry animation. */ |
| 681 | 594 | .hero-float { |
| 682 | 595 | position: absolute; |
| 683 | 596 | inset: 0; |
| 684 | 597 | pointer-events: none; |
| 685 | 598 | z-index: 1; |
| 686 | | --px: 0px; |
| 687 | | --py: 0px; |
| 688 | 599 | } |
| 689 | 600 | |
| 690 | 601 | .hero-float-mark { |
| ... | ... | @@ -694,8 +605,6 @@ const mobileEndorsementRows = [ |
| 694 | 605 | width: var(--s); |
| 695 | 606 | height: var(--s); |
| 696 | 607 | margin-left: calc(var(--s) / -2); |
| 697 | | translate: calc(var(--px) * var(--depth)) calc(var(--py) * var(--depth)); |
| 698 | | transition: translate 0.7s cubic-bezier(0.2, 0.7, 0.2, 1); |
| 699 | 608 | } |
| 700 | 609 | |
| 701 | 610 | .hero-float-card { |
| ... | ... | @@ -709,9 +618,7 @@ const mobileEndorsementRows = [ |
| 709 | 618 | box-shadow: |
| 710 | 619 | 0 20px 48px -16px rgba(0, 0, 0, 0.65), |
| 711 | 620 | 0 2px 0 0 rgba(255, 255, 255, 0.04) inset; |
| 712 | | animation: |
| 713 | | mark-in 1s cubic-bezier(0.2, 0.7, 0.2, 1) var(--d) both, |
| 714 | | mark-drift var(--dur) ease-in-out calc(var(--d) + 1s) infinite; |
| 621 | animation: mark-in 1s cubic-bezier(0.2, 0.7, 0.2, 1) var(--d) both; |
| 715 | 622 | } |
| 716 | 623 | |
| 717 | 624 | .hero-float-card img { |
| ... | ... | @@ -730,21 +637,16 @@ const mobileEndorsementRows = [ |
| 730 | 637 | from { opacity: 0; transform: translate(var(--fx), var(--fy)) rotate(calc(var(--rot) + 24deg)) scale(0.6); } |
| 731 | 638 | to { opacity: 1; transform: translate(0, 0) rotate(var(--rot)) scale(1); } |
| 732 | 639 | } |
| 733 | | @keyframes mark-drift { |
| 734 | | 0%, 100% { transform: translate(0, 0) rotate(var(--rot)); } |
| 735 | | 50% { transform: translate(var(--dx), var(--dy)) rotate(calc(var(--rot) + var(--wob))); } |
| 736 | | } |
| 737 | 640 | |
| 738 | | .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; } |
| 739 | | .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; } |
| 740 | | .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); } |
| 741 | | .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; } |
| 742 | | .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; } |
| 743 | | .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; } |
| 641 | .hf-opencode { --x: max(-300px, -19vw); --y: 20px; --s: 64px; --rot: 6deg; --d: 620ms; --fx: -40px; --fy: -80px; } |
| 642 | .hf-antigravity { --x: min(300px, 19vw); --y: 20px; --s: 64px; --rot: -5deg; --d: 580ms; --fx: 40px; --fy: -80px; } |
| 643 | .hf-claude { --x: max(-560px, -38vw); --y: 128px; --s: 96px; --rot: -8deg; --d: 380ms; --fx: -160px; --fy: -40px; --tint: rgba(217, 119, 87, 0.22); } |
| 644 | .hf-codex { --x: min(560px, 38vw); --y: 128px; --s: 96px; --rot: 7deg; --d: 460ms; --fx: 160px; --fy: -40px; } |
| 645 | .hf-grok { --x: max(-440px, -30vw); --y: 440px; --s: 80px; --rot: 4deg; --d: 540ms; --fx: -120px; --fy: 80px; } |
| 646 | .hf-cursor { --x: min(440px, 30vw); --y: 440px; --s: 80px; --rot: -5deg; --d: 700ms; --fx: 120px; --fy: 80px; } |
| 744 | 647 | |
| 745 | 648 | @media (prefers-reduced-motion: reduce) { |
| 746 | 649 | .hero-float-card { animation: none; transform: rotate(var(--rot)); } |
| 747 | | .hero-float-mark { transition: none; } |
| 748 | 650 | } |
| 749 | 651 | |
| 750 | 652 | @media (max-width: 1180px) { |
| ... | ... | @@ -767,12 +669,12 @@ const mobileEndorsementRows = [ |
| 767 | 669 | } |
| 768 | 670 | |
| 769 | 671 | /* Four above the headline, two beside the CTA. Keeps the copy clear. */ |
| 770 | | .hf-claude { --x: -37vw; --y: 44px; --s: 64px; --rot: -10deg; } |
| 771 | | .hf-opencode { --x: -12vw; --y: 52px; --s: 56px; --rot: -4deg; } |
| 772 | | .hf-antigravity { --x: 12vw; --y: 52px; --s: 56px; --rot: 5deg; } |
| 773 | | .hf-codex { --x: 37vw; --y: 44px; --s: 64px; --rot: 8deg; } |
| 672 | .hf-claude { --x: -37vw; --y: 44px; --s: 64px; --rot: -10deg; } |
| 673 | .hf-opencode { --x: -12vw; --y: 52px; --s: 56px; --rot: -4deg; } |
| 674 | .hf-antigravity { --x: 12vw; --y: 52px; --s: 56px; --rot: 5deg; } |
| 675 | .hf-codex { --x: 37vw; --y: 44px; --s: 64px; --rot: 8deg; } |
| 774 | 676 | .hf-grok { --x: -36vw; --y: 474px; --s: 64px; --rot: -6deg; } |
| 775 | | .hf-cursor { --x: 36vw; --y: 474px; --s: 64px; --rot: 7deg; } |
| 677 | .hf-cursor { --x: 36vw; --y: 474px; --s: 64px; --rot: 7deg; } |
| 776 | 678 | } |
| 777 | 679 | |
| 778 | 680 | @media (max-width: 340px) { |
| ... | ... | @@ -896,67 +798,19 @@ const mobileEndorsementRows = [ |
| 896 | 798 | } |
| 897 | 799 | |
| 898 | 800 | .endorsement-carousel { |
| 899 | | position: relative; |
| 900 | | display: flex; |
| 901 | | flex-direction: column; |
| 902 | | gap: 12px; |
| 903 | | } |
| 904 | | |
| 905 | | .endorsement-carousel--mobile { |
| 906 | | display: none; |
| 907 | | } |
| 908 | | |
| 909 | | .endorsement-carousel::before, |
| 910 | | .endorsement-carousel::after { |
| 911 | | content: ""; |
| 912 | | position: absolute; |
| 913 | | top: 0; |
| 914 | | bottom: 0; |
| 915 | | z-index: 2; |
| 916 | | width: max(48px, calc((100vw - 1240px) / 2 + 56px)); |
| 917 | | pointer-events: none; |
| 918 | | } |
| 919 | | |
| 920 | | .endorsement-carousel::before { |
| 921 | | left: 0; |
| 922 | | background: linear-gradient(90deg, var(--bg), transparent); |
| 923 | | } |
| 924 | | |
| 925 | | .endorsement-carousel::after { |
| 926 | | right: 0; |
| 927 | | background: linear-gradient(270deg, var(--bg), transparent); |
| 928 | | } |
| 929 | | |
| 930 | | .endorsement-row { |
| 931 | | overflow: hidden; |
| 932 | | } |
| 933 | | |
| 934 | | .endorsement-track { |
| 935 | | width: max-content; |
| 936 | | display: flex; |
| 937 | | animation: endorsementMarquee 76s linear infinite; |
| 938 | | will-change: transform; |
| 939 | | } |
| 940 | | |
| 941 | | .endorsement-track-group { |
| 942 | | display: flex; |
| 801 | display: grid; |
| 802 | grid-auto-flow: column; |
| 803 | grid-template-rows: repeat(2, 132px); |
| 804 | grid-auto-columns: clamp(264px, 22vw, 308px); |
| 943 | 805 | gap: 12px; |
| 944 | | padding-right: 12px; |
| 945 | | } |
| 946 | | |
| 947 | | .endorsement-row.is-reverse .endorsement-track { |
| 948 | | animation-direction: reverse; |
| 949 | | animation-duration: 84s; |
| 950 | | } |
| 951 | | |
| 952 | | .endorsement-carousel:hover .endorsement-track, |
| 953 | | .endorsement-carousel:focus-within .endorsement-track { |
| 954 | | animation-play-state: paused; |
| 806 | overflow-x: auto; |
| 807 | overscroll-behavior-x: contain; |
| 808 | padding: 4px max(32px, calc((100vw - 1240px) / 2 + 32px)); |
| 809 | scroll-padding-inline: max(32px, calc((100vw - 1240px) / 2 + 32px)); |
| 955 | 810 | } |
| 956 | 811 | |
| 957 | 812 | .endorsement-card { |
| 958 | 813 | position: relative; |
| 959 | | width: clamp(264px, 22vw, 308px); |
| 960 | 814 | height: 132px; |
| 961 | 815 | padding: 14px 16px; |
| 962 | 816 | display: flex; |
| ... | ... | @@ -1134,7 +988,6 @@ const mobileEndorsementRows = [ |
| 1134 | 988 | width: 7px; height: 14px; |
| 1135 | 989 | background: var(--fg); |
| 1136 | 990 | margin-left: 4px; |
| 1137 | | animation: blink 1s steps(2) infinite; |
| 1138 | 991 | } |
| 1139 | 992 | |
| 1140 | 993 | .open-pitch { |
| ... | ... | @@ -1252,19 +1105,6 @@ const mobileEndorsementRows = [ |
| 1252 | 1105 | margin-bottom: 24px; |
| 1253 | 1106 | } |
| 1254 | 1107 | |
| 1255 | | @keyframes blink { |
| 1256 | | 50% { opacity: 0; } |
| 1257 | | } |
| 1258 | | |
| 1259 | | @keyframes endorsementMarquee { |
| 1260 | | from { |
| 1261 | | transform: translateX(0); |
| 1262 | | } |
| 1263 | | to { |
| 1264 | | transform: translateX(-50%); |
| 1265 | | } |
| 1266 | | } |
| 1267 | | |
| 1268 | 1108 | /* ── Responsive ────────────────────────────────────────── */ |
| 1269 | 1109 | @media (max-width: 960px) { |
| 1270 | 1110 | .hero-preview { |
| ... | ... | @@ -1291,25 +1131,13 @@ const mobileEndorsementRows = [ |
| 1291 | 1131 | .endorsements-head { |
| 1292 | 1132 | margin-bottom: 40px; |
| 1293 | 1133 | } |
| 1294 | | .endorsement-carousel--desktop { |
| 1295 | | display: none; |
| 1296 | | } |
| 1297 | | .endorsement-carousel--mobile { |
| 1298 | | display: flex; |
| 1299 | | } |
| 1300 | | .endorsement-carousel::before, |
| 1301 | | .endorsement-carousel::after { |
| 1302 | | width: 28px; |
| 1303 | | } |
| 1304 | | .endorsement-track { |
| 1305 | | animation-duration: 68s; |
| 1306 | | } |
| 1307 | | .endorsement-row.is-reverse .endorsement-track { |
| 1308 | | animation-duration: 76s; |
| 1134 | .endorsement-carousel { |
| 1135 | grid-template-rows: repeat(3, 132px); |
| 1136 | grid-auto-columns: 268px; |
| 1137 | padding-inline: 20px; |
| 1138 | scroll-padding-inline: 20px; |
| 1309 | 1139 | } |
| 1310 | 1140 | .endorsement-card { |
| 1311 | | width: 268px; |
| 1312 | | height: 132px; |
| 1313 | 1141 | padding: 14px; |
| 1314 | 1142 | } |
| 1315 | 1143 | .endorsement-card p { |