├── .prettierrc
├── .eslintrc.json
├── app
├── favicon.ico
├── twitter-image.png
├── opengraph-image.png
├── not-found.tsx
├── page.tsx
├── layout.tsx
└── styles.tsx
├── postcss.config.js
├── types
├── review.d.ts
└── product.d.ts
├── public
├── nextjs-icon-light-background.png
├── patrick-OIFgeLnjwrM-unsplash.jpg
├── eniko-kis-KsLPTsYaqIQ-unsplash.jpg
├── prince-akachi-LWkFHEGpleE-unsplash.jpg
├── yoann-siloine-_T4w3JDm6ug-unsplash.jpg
├── guillaume-coupy-6HuoHgK7FN8-unsplash.jpg
├── alexander-andrews-brAkTCdnhW8-unsplash.jpg
└── grid.svg
├── components
├── product-best-seller.tsx
├── cart-count.tsx
├── product-low-stock-warning.tsx
├── ping.tsx
├── product-rating.tsx
├── product-currency-symbol.tsx
├── product-used-price.tsx
├── product-split-payments.tsx
├── product-estimated-arrival.tsx
├── product-review-card.tsx
├── product-lightening-deal.tsx
├── cart-count-context.tsx
├── product-deal.tsx
├── byline.tsx
├── product-price.tsx
├── next-logo.tsx
├── vercel-logo.tsx
├── reviews.tsx
├── add-to-cart.tsx
├── product-card.tsx
├── header.tsx
├── recommended-products.tsx
├── single-product.tsx
├── pricing.tsx
└── sidebar.tsx
├── next.config.js
├── next-env.d.ts
├── lib
└── delay.ts
├── .gitignore
├── tsconfig.json
├── package.json
├── README.md
└── tailwind.config.ts
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["next/core-web-vitals"]
4 | }
5 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/app/twitter-image.png
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/app/opengraph-image.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/types/review.d.ts:
--------------------------------------------------------------------------------
1 | export type Review = {
2 | id: string;
3 | name: string;
4 | rating: number;
5 | text: string;
6 | };
7 |
--------------------------------------------------------------------------------
/public/nextjs-icon-light-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/nextjs-icon-light-background.png
--------------------------------------------------------------------------------
/public/patrick-OIFgeLnjwrM-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/patrick-OIFgeLnjwrM-unsplash.jpg
--------------------------------------------------------------------------------
/public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/eniko-kis-KsLPTsYaqIQ-unsplash.jpg
--------------------------------------------------------------------------------
/public/prince-akachi-LWkFHEGpleE-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/prince-akachi-LWkFHEGpleE-unsplash.jpg
--------------------------------------------------------------------------------
/public/yoann-siloine-_T4w3JDm6ug-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/yoann-siloine-_T4w3JDm6ug-unsplash.jpg
--------------------------------------------------------------------------------
/public/guillaume-coupy-6HuoHgK7FN8-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/guillaume-coupy-6HuoHgK7FN8-unsplash.jpg
--------------------------------------------------------------------------------
/public/alexander-andrews-brAkTCdnhW8-unsplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vercel-labs/next-partial-prerendering/HEAD/public/alexander-andrews-brAkTCdnhW8-unsplash.jpg
--------------------------------------------------------------------------------
/components/product-best-seller.tsx:
--------------------------------------------------------------------------------
1 | export const ProductBestSeller = () => {
2 | return (
3 |
4 | Best Seller
5 |
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | // Removed experimental.ppr as it's only available in canary versions
4 | // and not compatible with Next.js 15.0.5+
5 | };
6 |
7 | module.exports = nextConfig;
8 |
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFound() {
2 | return (
3 |
4 |
Not Found
5 |
Could not find requested resource
6 |
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | import "./.next/types/routes.d.ts";
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
7 |
--------------------------------------------------------------------------------
/components/cart-count.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useCartCount } from '#/components/cart-count-context';
4 |
5 | export function CartCount({ initialCartCount }: { initialCartCount: number }) {
6 | const [count] = useCartCount(initialCartCount);
7 | return {count} ;
8 | }
9 |
--------------------------------------------------------------------------------
/components/product-low-stock-warning.tsx:
--------------------------------------------------------------------------------
1 | export const ProductLowStockWarning = ({ stock }: { stock: number }) => {
2 | if (stock > 3) {
3 | return null;
4 | }
5 |
6 | if (stock === 0) {
7 | return Out of stock
;
8 | }
9 |
10 | return (
11 | Only {stock} left in stock
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/lib/delay.ts:
--------------------------------------------------------------------------------
1 | // Times are in milliseconds
2 | export const delayShippingEstimate = 200;
3 | export const delayRecommendedProducts = 500;
4 | export const delayReviews = 600;
5 |
6 | export async function withDelay(
7 | promise: Promise,
8 | delay: number
9 | ): Promise {
10 | // Ensure we throw if this throws
11 | const ret = await promise;
12 | return new Promise((resolve) => {
13 | setTimeout(() => {
14 | resolve(ret);
15 | }, delay);
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/components/ping.tsx:
--------------------------------------------------------------------------------
1 | export function Ping() {
2 | return (
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/components/product-rating.tsx:
--------------------------------------------------------------------------------
1 | import { StarIcon } from '@heroicons/react/24/solid';
2 | import clsx from 'clsx';
3 |
4 | export const ProductRating = ({ rating }: { rating: number }) => {
5 | return (
6 |
7 | {Array.from({ length: 5 }).map((_, i) => {
8 | return (
9 |
13 | );
14 | })}
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | /.yarn
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*
31 | !.env*.example
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 |
--------------------------------------------------------------------------------
/components/product-currency-symbol.tsx:
--------------------------------------------------------------------------------
1 | import { toFormat, type Dinero } from 'dinero.js';
2 |
3 | export const ProductCurrencySymbol = ({
4 | dinero,
5 | }: {
6 | dinero: Dinero;
7 | }) => {
8 | let symbol = '';
9 | switch (toFormat(dinero, ({ currency }) => currency.code)) {
10 | case 'GBP': {
11 | symbol = '£';
12 | break;
13 | }
14 |
15 | case 'EUR': {
16 | symbol = '€';
17 | break;
18 | }
19 |
20 | default: {
21 | symbol = '$';
22 | break;
23 | }
24 | }
25 |
26 | return <>{symbol}>;
27 | };
28 |
--------------------------------------------------------------------------------
/components/product-used-price.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '#/types/product';
2 | import { dinero, toUnit, up, type DineroSnapshot } from 'dinero.js';
3 |
4 | export const ProductUsedPrice = ({
5 | usedPrice: usedPriceRaw,
6 | }: {
7 | usedPrice: Product['usedPrice'];
8 | }) => {
9 | const usedPrice = dinero(usedPriceRaw as DineroSnapshot);
10 |
11 | return (
12 |
13 |
More buying choices
14 |
15 | ${toUnit(usedPrice, { digits: 0, round: up })} (used)
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components/product-split-payments.tsx:
--------------------------------------------------------------------------------
1 | import { ProductCurrencySymbol } from '#/components/product-currency-symbol';
2 | import { allocate, toUnit, up, type Dinero } from 'dinero.js';
3 |
4 | export const ProductSplitPayments = ({ price }: { price: Dinero }) => {
5 | // only offer split payments for more expensive items
6 | if (toUnit(price) < 150) {
7 | return null;
8 | }
9 |
10 | const [perMonth] = allocate(price, [1, 2]);
11 | return (
12 |
13 | Or
14 | {toUnit(perMonth, { digits: 0, round: up })}/month for 3 months
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/components/product-estimated-arrival.tsx:
--------------------------------------------------------------------------------
1 | import { add, format, isTomorrow } from 'date-fns';
2 |
3 | export const ProductEstimatedArrival = ({
4 | leadTime,
5 | hasDeliveryTime = false,
6 | }: {
7 | leadTime: number;
8 | hasDeliveryTime?: boolean;
9 | }) => {
10 | const date = add(new Date(), {
11 | days: leadTime,
12 | });
13 |
14 | return (
15 |
16 | Get it{' '}
17 |
18 | {isTomorrow(date) ? 'tomorrow, ' : null}
19 | {format(date, 'MMM d')}
20 |
21 | {hasDeliveryTime ? <> by 5pm> : null}
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/product-review-card.tsx:
--------------------------------------------------------------------------------
1 | import type { Review } from '#/types/review';
2 | import { ProductRating } from '#/components/product-rating';
3 |
4 | export const ProductReviewCard = ({ review }: { review: Review }) => {
5 | return (
6 |
7 |
8 |
9 |
10 |
{review.name}
11 |
12 |
13 | {review.rating ?
: null}
14 |
15 |
16 |
{review.text}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/types/product.d.ts:
--------------------------------------------------------------------------------
1 | export type Product = {
2 | id: string;
3 | stock: number;
4 | rating: number;
5 | name: string;
6 | description: string;
7 | price: Price;
8 | isBestSeller: boolean;
9 | leadTime: number;
10 | image?: string;
11 | imageBlur?: string;
12 | discount?: Discount;
13 | usedPrice?: UsedPrice;
14 | };
15 |
16 | type Price = {
17 | amount: number;
18 | currency: Currency;
19 | scale: number;
20 | };
21 |
22 | type Currency = {
23 | code: string;
24 | base: number;
25 | exponent: number;
26 | };
27 |
28 | type Discount = {
29 | percent: number;
30 | expires?: number;
31 | };
32 |
33 | type UsedPrice = {
34 | amount: number;
35 | currency: Currency;
36 | scale: number;
37 | };
38 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from 'react';
2 | import {
3 | RecommendedProducts,
4 | RecommendedProductsSkeleton,
5 | } from '#/components/recommended-products';
6 | import { Reviews, ReviewsSkeleton } from '#/components/reviews';
7 | import { SingleProduct } from '#/components/single-product';
8 | import { Ping } from '#/components/ping';
9 |
10 | export default function Page() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
}>
18 |
19 |
20 |
21 |
22 |
23 |
}>
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/components/product-lightening-deal.tsx:
--------------------------------------------------------------------------------
1 | import { ProductDeal } from '#/components/product-deal';
2 | import { add, formatDistanceToNow } from 'date-fns';
3 | import { type Dinero } from 'dinero.js';
4 |
5 | export const ProductLighteningDeal = ({
6 | price,
7 | discount,
8 | }: {
9 | price: Dinero;
10 | discount: {
11 | amount: Dinero;
12 | expires?: number;
13 | };
14 | }) => {
15 | const date = add(new Date(), { days: discount.expires });
16 |
17 | return (
18 | <>
19 |
20 |
21 | Expires in {formatDistanceToNow(date)}
22 |
23 |
24 |
25 |
26 | >
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "react-jsx",
20 | "incremental": true,
21 | "baseUrl": ".",
22 | "paths": {
23 | "#/*": [
24 | "./*"
25 | ]
26 | },
27 | "plugins": [
28 | {
29 | "name": "next"
30 | }
31 | ]
32 | },
33 | "include": [
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | ".next/types/**/*.ts"
38 | ],
39 | "exclude": [
40 | "node_modules"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "type-check": "tsc --noEmit",
5 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
6 | "build": "next build",
7 | "dev": "next dev --turbo",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@heroicons/react": "2.1.5",
12 | "clsx": "2.1.1",
13 | "date-fns": "3.6.0",
14 | "dinero.js": "2.0.0-alpha.8",
15 | "geist": "1.3.1",
16 | "next": "15.6.0-canary.60",
17 | "react": "19.0.0-rc-8b08e99e-20240713",
18 | "react-dom": "19.0.0-rc-8b08e99e-20240713"
19 | },
20 | "devDependencies": {
21 | "@tailwindcss/forms": "0.5.7",
22 | "@tailwindcss/typography": "0.5.13",
23 | "@types/node": "20.14.10",
24 | "@types/react": "18.3.3",
25 | "@types/react-dom": "18.3.0",
26 | "autoprefixer": "10.4.19",
27 | "eslint": "8.57.1",
28 | "eslint-config-next": "15.6.0-canary.58",
29 | "postcss": "8.4.39",
30 | "tailwindcss": "3.4.5",
31 | "typescript": "5.5.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/components/cart-count-context.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 |
5 | const CartCountContext = React.createContext<
6 | | [null | number, React.Dispatch>]
7 | | undefined
8 | >(undefined);
9 |
10 | export function CartCountProvider({ children }: { children: React.ReactNode }) {
11 | const [optimisticCartCount, setOptimisticCartCount] = useState(
12 | null,
13 | );
14 |
15 | return (
16 |
19 | {children}
20 |
21 | );
22 | }
23 |
24 | export function useCartCount(
25 | initialCount: number,
26 | ): [null | number, React.Dispatch>] {
27 | const context = React.useContext(CartCountContext);
28 | if (context === undefined) {
29 | throw new Error('useCartCount must be used within a CartCountProvider');
30 | }
31 | if (context[0] === null) {
32 | return [initialCount, context[1]];
33 | }
34 | return context;
35 | }
36 |
--------------------------------------------------------------------------------
/components/product-deal.tsx:
--------------------------------------------------------------------------------
1 | import { ProductCurrencySymbol } from '#/components/product-currency-symbol';
2 | import { toUnit, type Dinero } from 'dinero.js';
3 |
4 | export const ProductDeal = ({
5 | price: priceRaw,
6 | discount: discountRaw,
7 | }: {
8 | price: Dinero;
9 | discount: {
10 | amount: Dinero;
11 | };
12 | }) => {
13 | const discount = toUnit(discountRaw.amount);
14 | const price = toUnit(priceRaw);
15 | const percent = Math.round(100 - (discount / price) * 100);
16 |
17 | return (
18 |
19 |
20 | -{percent}%
21 |
22 |
23 |
26 |
27 | {discount}
28 |
29 |
30 |
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/components/byline.tsx:
--------------------------------------------------------------------------------
1 | import { VercelLogo } from '#/components/vercel-logo';
2 |
3 | export default function Byline({ className }: { className: string }) {
4 | return (
5 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Next.js Partial Prerendering
2 |
3 | This is a demo of [Next.js](https://nextjs.org) using [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr).
4 |
5 | This template uses the new Next.js [App Router](https://nextjs.org/docs/app). This includes support for enhanced layouts, colocation of components, tests, and styles, component-level data fetching, and more.
6 |
7 | It also uses the experimental Partial Prerendering feature available in Next.js 14. Partial Prerendering combines ultra-quick static edge delivery with fully dynamic capabilities and we believe it has the potential to [become the default rendering model for web applications](https://vercel.com/blog/partial-prerendering-with-next-js-creating-a-new-default-rendering-model), bringing together the best of static site generation and dynamic delivery.
8 |
9 | > ⚠️ Please note that PPR is an experimental technology that is not recommended for production. You may run into some DX issues, especially on larger code bases.
10 |
11 | ## How it works
12 |
13 | The index route `/` uses Partial Prerendering through:
14 |
15 | 1. Enabling the experimental flag in `next.config.js`.
16 |
17 | ```js
18 | experimental: {
19 | ppr: true,
20 | },
21 | ```
22 |
23 | 2. Using ` ` to wrap Dynamic content.
24 |
--------------------------------------------------------------------------------
/public/grid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/components/product-price.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '#/types/product';
2 | import { ProductCurrencySymbol } from '#/components/product-currency-symbol';
3 | import { ProductDeal } from '#/components/product-deal';
4 | import { ProductLighteningDeal } from '#/components/product-lightening-deal';
5 | import { multiply, toUnit, type Dinero } from 'dinero.js';
6 |
7 | function isDiscount(obj: any): obj is { percent: number; expires?: number } {
8 | return typeof obj?.percent === 'number';
9 | }
10 |
11 | function formatDiscount(
12 | price: Dinero,
13 | discountRaw: Product['discount'],
14 | ) {
15 | return isDiscount(discountRaw)
16 | ? {
17 | amount: multiply(price, {
18 | amount: discountRaw.percent,
19 | scale: 2,
20 | }),
21 | expires: discountRaw.expires,
22 | }
23 | : undefined;
24 | }
25 |
26 | export const ProductPrice = ({
27 | price,
28 | discount: discountRaw,
29 | }: {
30 | price: Dinero;
31 | discount: Product['discount'];
32 | }) => {
33 | const discount = formatDiscount(price, discountRaw);
34 |
35 | if (discount) {
36 | if (discount?.expires && typeof discount.expires === 'number') {
37 | return ;
38 | }
39 | return ;
40 | }
41 |
42 | return (
43 |
44 |
47 |
48 | {toUnit(price)}
49 |
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/components/next-logo.tsx:
--------------------------------------------------------------------------------
1 | export function NextLogo() {
2 | return (
3 |
4 |
12 |
13 |
14 |
15 |
16 |
20 |
27 |
28 |
29 |
37 |
38 |
39 |
40 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { CartCountProvider } from "#/components/cart-count-context";
2 | import { Header } from "#/components/header";
3 | import { Sidebar } from "#/components/sidebar";
4 | import { Metadata } from "next";
5 | import { GlobalStyles } from "./styles";
6 |
7 | export const metadata: Metadata = {
8 | metadataBase: new URL("https://partialprerendering.com"),
9 | title: "Next.js Partial Prerendering",
10 | description: "A demo of Next.js using Partial Prerendering.",
11 | openGraph: {
12 | title: "Next.js Partial Prerendering",
13 | description: "A demo of Next.js using Partial Prerendering.",
14 | },
15 | twitter: {
16 | card: "summary_large_image",
17 | },
18 | };
19 |
20 | export default function RootLayout({
21 | children,
22 | }: {
23 | children: React.ReactNode;
24 | }) {
25 | return (
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {children}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/vercel-logo.tsx:
--------------------------------------------------------------------------------
1 | export function VercelLogo() {
2 | return (
3 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/components/reviews.tsx:
--------------------------------------------------------------------------------
1 | import type { Review } from '#/types/review';
2 | import { ProductReviewCard } from '#/components/product-review-card';
3 | import { delayReviews, withDelay } from '#/lib/delay';
4 |
5 | export async function Reviews() {
6 | let reviews: Review[] = await withDelay(
7 | fetch(
8 | // We intentionally delay the response to simulate a slow data
9 | // request that would benefit from streaming
10 | `https://app-router-api.vercel.app/api/reviews`,
11 | {
12 | // We intentionally disable Next.js Cache to better demo
13 | // streaming
14 | cache: 'no-store',
15 | }
16 | ).then((res) => res.json()),
17 | delayReviews
18 | );
19 |
20 | return (
21 |
22 |
Customer Reviews
23 |
24 | {reviews.map((review) => {
25 | return
;
26 | })}
27 |
28 |
29 | );
30 | }
31 |
32 | const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`;
33 |
34 | function Skeleton() {
35 | return (
36 |
42 | );
43 | }
44 |
45 | export function ReviewsSkeleton() {
46 | return (
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/components/add-to-cart.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useRouter } from 'next/navigation';
4 | import { useTransition } from 'react';
5 | import { useCartCount } from '#/components/cart-count-context';
6 |
7 | export function AddToCart({ initialCartCount }: { initialCartCount: number }) {
8 | const router = useRouter();
9 | const [isPending, startTransition] = useTransition();
10 |
11 | const [, setOptimisticCartCount] = useCartCount(initialCartCount);
12 |
13 | const addToCart = () => {
14 | setOptimisticCartCount(initialCartCount + 1);
15 |
16 | // update the cart count cookie
17 | document.cookie = `_cart_count=${initialCartCount + 1}; path=/; max-age=${
18 | 60 * 60 * 24 * 30
19 | }};`;
20 |
21 | // Normally you would also send a request to the server to add the item
22 | // to the current users cart
23 | // await fetch(`https://api.acme.com/...`);
24 |
25 | // Use a transition and isPending to create inline loading UI
26 | startTransition(() => {
27 | setOptimisticCartCount(null);
28 |
29 | // Refresh the current route and fetch new data from the server without
30 | // losing client-side browser or React state.
31 | router.refresh();
32 |
33 | // We're working on more fine-grained data mutation and revalidation:
34 | // https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
35 | });
36 | };
37 |
38 | return (
39 |
44 | Add to Cart
45 | {isPending ? (
46 |
47 |
51 |
Loading...
52 |
53 | ) : null}
54 |
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/components/product-card.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '#/types/product';
2 | import { ProductBestSeller } from '#/components/product-best-seller';
3 | import { ProductEstimatedArrival } from '#/components/product-estimated-arrival';
4 | import { ProductLowStockWarning } from '#/components/product-low-stock-warning';
5 | import { ProductPrice } from '#/components/product-price';
6 | import { ProductRating } from '#/components/product-rating';
7 | import { ProductUsedPrice } from '#/components/product-used-price';
8 | import { dinero, type DineroSnapshot } from 'dinero.js';
9 | import Image from 'next/image';
10 |
11 | export const ProductCard = ({ product }: { product: Product }) => {
12 | const price = dinero(product.price as DineroSnapshot);
13 |
14 | return (
15 |
16 |
17 |
18 | {product.isBestSeller ? (
19 |
22 | ) : null}
23 |
32 |
33 |
34 |
35 | {product.name}
36 |
37 |
38 | {product.rating ?
: null}
39 |
40 |
41 |
42 | {product.usedPrice ? (
43 |
44 | ) : null}
45 |
46 |
47 |
48 | {product.stock <= 1 ? (
49 |
50 | ) : null}
51 |
52 |
53 | );
54 | };
55 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { NextLogo } from '#/components/next-logo';
2 | import {
3 | MagnifyingGlassIcon,
4 | ShoppingCartIcon,
5 | } from '@heroicons/react/24/solid';
6 | import Image from 'next/image';
7 | import { CartCount } from '#/components/cart-count';
8 | import { cookies } from 'next/headers';
9 | import { Suspense } from 'react';
10 |
11 | async function CartCountFromCookies() {
12 | const cartCount = Number((await cookies()).get('_cart_count')?.value || '0');
13 | return ;
14 | }
15 |
16 | export function Header() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | }>
44 |
45 |
46 |
47 |
48 |
49 |
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import colors from 'tailwindcss/colors';
2 | import { Config } from 'tailwindcss';
3 |
4 | export default {
5 | content: [
6 | './app/**/*.{js,ts,jsx,tsx,mdx}',
7 | './components/**/*.{js,ts,jsx,tsx,mdx}',
8 | ],
9 | future: {
10 | hoverOnlyWhenSupported: true,
11 | },
12 | darkMode: 'class',
13 | theme: {
14 | extend: {
15 | // https://vercel.com/design/color
16 | colors: {
17 | gray: colors.zinc,
18 | 'gray-1000': 'rgb(17,17,19)',
19 | 'gray-1100': 'rgb(10,10,11)',
20 | vercel: {
21 | pink: '#FF0080',
22 | blue: '#0070F3',
23 | cyan: '#50E3C2',
24 | orange: '#F5A623',
25 | violet: '#7928CA',
26 | },
27 | },
28 | backgroundImage: ({ theme }) => ({
29 | 'vc-border-gradient': `radial-gradient(at left top, ${theme(
30 | 'colors.gray.500',
31 | )}, 50px, ${theme('colors.gray.800')} 50%)`,
32 | }),
33 | keyframes: ({ theme }) => ({
34 | rerender: {
35 | '0%': {
36 | ['border-color']: theme('colors.vercel.pink'),
37 | },
38 | '40%': {
39 | ['border-color']: theme('colors.vercel.pink'),
40 | },
41 | },
42 | highlight: {
43 | '0%': {
44 | background: theme('colors.vercel.pink'),
45 | color: theme('colors.white'),
46 | },
47 | '40%': {
48 | background: theme('colors.vercel.pink'),
49 | color: theme('colors.white'),
50 | },
51 | },
52 | loading: {
53 | '0%': {
54 | opacity: '.2',
55 | },
56 | '20%': {
57 | opacity: '1',
58 | transform: 'translateX(1px)',
59 | },
60 | to: {
61 | opacity: '.2',
62 | },
63 | },
64 | shimmer: {
65 | '100%': {
66 | transform: 'translateX(100%)',
67 | },
68 | },
69 | translateXReset: {
70 | '100%': {
71 | transform: 'translateX(0)',
72 | },
73 | },
74 | fadeToTransparent: {
75 | '0%': {
76 | opacity: '1',
77 | },
78 | '40%': {
79 | opacity: '1',
80 | },
81 | '100%': {
82 | opacity: '0',
83 | },
84 | },
85 | }),
86 | },
87 | },
88 | plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
89 | } satisfies Config;
90 |
--------------------------------------------------------------------------------
/components/recommended-products.tsx:
--------------------------------------------------------------------------------
1 | import { Product } from '#/types/product';
2 | import { ProductCard } from '#/components/product-card';
3 | import { delayRecommendedProducts, withDelay } from '#/lib/delay';
4 |
5 | export async function RecommendedProducts() {
6 | let products: Product[] = await withDelay(
7 | fetch(
8 | // We intentionally delay the response to simulate a slow data
9 | // request that would benefit from streaming
10 | `https://app-router-api.vercel.app/api/products?filter=1`,
11 | {
12 | // We intentionally disable Next.js Cache to better demo
13 | // streaming
14 | cache: 'no-store',
15 | }
16 | ).then((res) => res.json()),
17 | delayRecommendedProducts
18 | );
19 |
20 | return (
21 |
22 |
23 |
24 | Recommended Products for You
25 |
26 |
27 | Based on your preferences and shopping habits
28 |
29 |
30 |
31 | {products.map((product) => (
32 |
35 | ))}
36 |
37 |
38 | );
39 | }
40 |
41 | const shimmer = `relative overflow-hidden before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_1.5s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent`;
42 |
43 | function ProductSkeleton() {
44 | return (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | export function RecommendedProductsSkeleton() {
57 | return (
58 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/single-product.tsx:
--------------------------------------------------------------------------------
1 | import { Pricing } from "#/components/pricing";
2 | import type { Product } from "#/types/product";
3 | import { ProductRating } from "#/components/product-rating";
4 | import Image from "next/image";
5 |
6 | export async function SingleProduct() {
7 | const product: Product = await fetch(
8 | `https://app-router-api.vercel.app/api/products?id=1`
9 | ).then((res) => res.json());
10 |
11 | return (
12 |
13 |
14 |
15 |
16 |
24 |
25 |
26 |
27 |
28 |
36 |
37 |
38 |
46 |
47 |
48 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 |
67 | {product.name}
68 |
69 |
70 |
71 |
72 |
73 |
{product.description}
74 |
{product.description}
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/components/pricing.tsx:
--------------------------------------------------------------------------------
1 | import type { Product } from '#/types/product';
2 | import { Ping } from '#/components/ping';
3 | import { ProductEstimatedArrival } from '#/components/product-estimated-arrival';
4 | import { ProductLowStockWarning } from '#/components/product-low-stock-warning';
5 | import { ProductPrice } from '#/components/product-price';
6 | import { ProductSplitPayments } from '#/components/product-split-payments';
7 | import { ProductUsedPrice } from '#/components/product-used-price';
8 | import { dinero, type DineroSnapshot } from 'dinero.js';
9 | import { Suspense } from 'react';
10 | import { AddToCart } from '#/components/add-to-cart';
11 | import { delayShippingEstimate, withDelay } from '#/lib/delay';
12 | import { cookies } from 'next/headers';
13 |
14 | async function AddToCartFromCookies() {
15 | // Get the cart count from the users cookies and pass it to the client
16 | // AddToCart component
17 | const cartCount = Number((await cookies()).get('_cart_count')?.value || '0');
18 | return ;
19 | }
20 |
21 | function LoadingDots() {
22 | return (
23 |
24 |
25 |
26 | •
27 |
28 |
29 | •
30 |
31 |
32 | •
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | async function UserSpecificDetails({ productId }: { productId: string }) {
40 | const data = await withDelay(
41 | fetch(
42 | `https://app-router-api.vercel.app/api/products?id=${productId}&filter=price,usedPrice,leadTime,stock`,
43 | {
44 | // We intentionally disable Next.js Cache to better demo
45 | // streaming
46 | cache: 'no-store',
47 | }
48 | ),
49 | delayShippingEstimate
50 | );
51 |
52 | const product = (await data.json()) as Product;
53 |
54 | const price = dinero(product.price as DineroSnapshot);
55 |
56 | return (
57 | <>
58 |
59 | {product.usedPrice ? (
60 |
61 | ) : null}
62 |
63 | {product.stock <= 1 ? (
64 |
65 | ) : null}
66 | >
67 | );
68 | }
69 |
70 | export function Pricing({ product }: { product: Product }) {
71 | const price = dinero(product.price as DineroSnapshot);
72 |
73 | return (
74 |
75 |
76 |
77 |
78 |
79 |
}>
80 |
81 |
82 |
83 |
}>
84 |
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { NextLogo } from '#/components/next-logo';
4 | import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/solid';
5 |
6 | import clsx from 'clsx';
7 | import { useState } from 'react';
8 | import Byline from '#/components/byline';
9 | import {
10 | delayRecommendedProducts,
11 | delayReviews,
12 | delayShippingEstimate,
13 | } from '#/lib/delay';
14 |
15 | export function Sidebar() {
16 | const [isOpen, setIsOpen] = useState(false);
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Partial Prerendering
28 |
29 |
30 |
31 |
setIsOpen(!isOpen)}
35 | >
36 |
37 | Menu
38 |
39 | {isOpen ? (
40 |
41 | ) : (
42 |
43 | )}
44 |
45 |
46 |
52 |
53 |
54 |
55 | Pink dots {' '}
56 | denote artificially delayed responses for demo purposes:
57 |
58 |
59 | Shipping estimate → {delayShippingEstimate}ms
60 | Recommended products → {delayRecommendedProducts}ms
61 | Reviews → {delayReviews}ms
62 |
63 |
64 |
65 |
66 |
70 | Partial Prerendering
71 | {' '}
72 | combines ultra-quick static edge delivery with fully dynamic
73 | capabilities. This is different from how your application behaves
74 | today, where entire routes are either fully static or dynamic.
75 |
76 |
How it works:
77 |
78 |
79 | A static route shell is served immediately, this makes
80 | the initial load fast.
81 |
82 |
83 | The shell leaves holes where dynamic content (that might
84 | be slower) will be streamed in to minimize the perceived overall
85 | page load time.
86 |
87 |
88 | The async holes are loaded in parallel, reducing the overall load
89 | time of the page.
90 |
91 |
92 |
93 | Try refreshing the page to restart the demo.
94 |
95 |
96 |
97 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/app/styles.tsx:
--------------------------------------------------------------------------------
1 | export function GlobalStyles() {
2 | return (
3 |
8 | );
9 | }
10 |
11 | const styles = JSON.parse(
12 | "\"/*\\n! tailwindcss v3.4.5 | MIT License | https://tailwindcss.com\\n*/*,:after,:before{box-sizing:border-box;border:0 solid #e4e4e7}:after,:before{--tw-content:\\\"\\\"}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#a1a1aa}input::placeholder,textarea::placeholder{color:#a1a1aa}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],input:where(:not([type])),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#71717a;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,input:where(:not([type])):focus,select:focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-color:#2563eb}input::-moz-placeholder,textarea::-moz-placeholder{color:#71717a;opacity:1}input::placeholder,textarea::placeholder{color:#71717a;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-top:0;padding-bottom:0}select{background-image:url(\\\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%2371717a' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e\\\");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple],[size]:where(select:not([size=\\\"1\\\"])){background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#71717a;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}[type=checkbox]:checked,[type=radio]:checked{border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url(\\\"data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e\\\")}@media (forced-colors:active){[type=checkbox]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=radio]:checked{background-image:url(\\\"data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e\\\")}@media (forced-colors:active){[type=radio]:checked{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url(\\\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e\\\");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}@media (forced-colors:active){[type=checkbox]:indeterminate{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);text-decoration:underline;font-weight:500}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=\\\"1\\\"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:disc;margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{font-weight:400;color:var(--tw-prose-counters)}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.25em}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-style:italic;color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:\\\"\\\\201C\\\"\\\"\\\\201D\\\"\\\"\\\\2018\\\"\\\"\\\\2019\\\";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:900;color:inherit}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:800;color:inherit}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:700;color:inherit}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){display:block;margin-top:2em;margin-bottom:2em}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:500;font-family:inherit;color:var(--tw-prose-kbd);box-shadow:0 0 0 1px rgb(var(--tw-prose-kbd-shadows)/10%),0 3px 0 rgb(var(--tw-prose-kbd-shadows)/10%);font-size:.875em;border-radius:.3125rem;padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;padding-inline-start:.375em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-weight:600;font-size:.875em}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:\\\"`\\\"}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:\\\"`\\\"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);overflow-x:auto;font-weight:400;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding-top:.8571429em;padding-inline-end:1.1428571em;padding-bottom:.8571429em;padding-inline-start:1.1428571em}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:none}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){width:100%;table-layout:auto;text-align:start;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);font-weight:600;vertical-align:bottom;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:17 24 39;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:255 255 255;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:rgba(0,0,0,.5);--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.5714286em;padding-inline-end:.5714286em;padding-bottom:.5714286em;padding-inline-start:.5714286em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-sm{font-size:.875rem;line-height:1.7142857}.prose-sm :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;line-height:1.5555556;margin-top:.8888889em;margin-bottom:.8888889em}.prose-sm :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.3333333em;margin-bottom:1.3333333em;padding-inline-start:1.1111111em}.prose-sm :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:2.1428571em;margin-top:0;margin-bottom:.8em;line-height:1.2}.prose-sm :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.4285714em;margin-top:1.6em;margin-bottom:.8em;line-height:1.4}.prose-sm :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:1.2857143em;margin-top:1.5555556em;margin-bottom:.4444444em;line-height:1.5555556}.prose-sm :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.4285714em;margin-bottom:.5714286em;line-height:1.4285714}.prose-sm :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;border-radius:.3125rem;padding-top:.1428571em;padding-inline-end:.3571429em;padding-bottom:.1428571em;padding-inline-start:.3571429em}.prose-sm :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em}.prose-sm :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.9em}.prose-sm :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8888889em}.prose-sm :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.6666667;margin-top:1.6666667em;margin-bottom:1.6666667em;border-radius:.25rem;padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em;padding-inline-start:1.5714286em}.prose-sm :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;margin-bottom:.2857143em}.prose-sm :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.4285714em}.prose-sm :where(.prose-sm>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(.prose-sm>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(.prose-sm>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.1428571em}.prose-sm :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5714286em;margin-bottom:.5714286em}.prose-sm :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em;margin-bottom:1.1428571em}.prose-sm :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.1428571em}.prose-sm :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.2857143em;padding-inline-start:1.5714286em}.prose-sm :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2.8571429em;margin-bottom:2.8571429em}.prose-sm :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.5}.prose-sm :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.6666667em;padding-inline-end:1em;padding-bottom:.6666667em;padding-inline-start:1em}.prose-sm :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose-sm :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose-sm :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.7142857em;margin-bottom:1.7142857em}.prose-sm :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose-sm :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){font-size:.8571429em;line-height:1.3333333;margin-top:.6666667em}.prose-sm :where(.prose-sm>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose-sm :where(.prose-sm>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.-left-4{left:-1rem}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-0{bottom:0}.bottom-3{bottom:.75rem}.left-0{left:0}.left-2{left:.5rem}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-1{top:.25rem}.top-1\\\\.5{top:.375rem}.top-14{top:3.5rem}.top-2{top:.5rem}.z-10{z-index:10}.z-20{z-index:20}.col-span-2{grid-column:span 2/span 2}.col-span-4{grid-column:span 4/span 4}.col-span-full{grid-column:1/-1}.mx-3{margin-left:.75rem;margin-right:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-px{margin-top:1px}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-\\\\[11px\\\\]{height:11px}.h-\\\\[167px\\\\]{height:167px}.h-full{height:100%}.w-1\\\\/2{width:50%}.w-1\\\\/3{width:33.333333%}.w-1\\\\/6{width:16.666667%}.w-10{width:2.5rem}.w-16{width:4rem}.w-2\\\\/5{width:40%}.w-2\\\\/6{width:33.333333%}.w-4{width:1rem}.w-4\\\\/6{width:66.666667%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-\\\\[11px\\\\]{width:11px}.w-full{width:100%}.max-w-4xl{max-width:56rem}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.animate-\\\\[loading_1\\\\.4s_ease-in-out_0\\\\.2s_infinite\\\\]{animation:loading 1.4s ease-in-out .2s infinite}.animate-\\\\[loading_1\\\\.4s_ease-in-out_0\\\\.4s_infinite\\\\]{animation:loading 1.4s ease-in-out .4s infinite}@keyframes loading{0%{opacity:.2}20%{opacity:1;transform:translateX(1px)}to{opacity:.2}}.animate-\\\\[loading_1\\\\.4s_ease-in-out_infinite\\\\]{animation:loading 1.4s ease-in-out infinite}@keyframes ping{75%,to{transform:scale(2);opacity:0}}.animate-ping{animation:ping 1s cubic-bezier(0,0,.2,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-6{gap:1.5rem}.gap-x-1{-moz-column-gap:.25rem;column-gap:.25rem}.gap-x-1\\\\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-2\\\\.5{-moz-column-gap:.625rem;column-gap:.625rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.space-x-0\\\\.5>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.125rem * var(--tw-space-x-reverse));margin-left:calc(.125rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-10>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2.5rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-\\\\[3px\\\\]{border-width:3px}.border-b{border-bottom-width:1px}.border-none{border-style:none}.border-gray-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.border-white\\\\/30{border-color:hsla(0,0%,100%,.3)}.border-r-transparent{border-right-color:transparent}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-gray-1100{--tw-bg-opacity:1;background-color:rgb(10 10 11/var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity))}.bg-vercel-blue{--tw-bg-opacity:1;background-color:rgb(0 112 243/var(--tw-bg-opacity))}.bg-vercel-cyan{--tw-bg-opacity:1;background-color:rgb(80 227 194/var(--tw-bg-opacity))}.bg-vercel-pink{--tw-bg-opacity:1;background-color:rgb(255 0 128/var(--tw-bg-opacity))}.bg-\\\\[url\\\\(\\\\'\\\\/grid\\\\.svg\\\\'\\\\)\\\\]{background-image:url(/grid.svg)}.bg-vc-border-gradient{background-image:radial-gradient(at left top,#71717a,50px,#27272a 50%)}.p-3{padding:.75rem}.p-3\\\\.5{padding:.875rem}.p-px{padding:1px}.px-1\\\\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pb-36{padding-bottom:9rem}.pb-\\\\[5px\\\\]{padding-bottom:5px}.pl-10{padding-left:2.5rem}.pl-3{padding-left:.75rem}.pt-20{padding-top:5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-5{line-height:1.25rem}.leading-snug{line-height:1.375}.tracking-wide{letter-spacing:.025em}.text-cyan-800{--tw-text-opacity:1;color:rgb(21 94 117/var(--tw-text-opacity))}.text-gray-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity))}.text-vercel-cyan{--tw-text-opacity:1;color:rgb(80 227 194/var(--tw-text-opacity))}.text-vercel-pink{--tw-text-opacity:1;color:rgb(255 0 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.decoration-dotted{text-decoration-style:dotted}.underline-offset-4{text-underline-offset:4px}.opacity-75{opacity:.75}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-black\\\\/20{--tw-shadow-color:rgba(0,0,0,.2);--tw-shadow:var(--tw-shadow-colored)}.blur{--tw-blur:blur(8px)}.blur,.grayscale{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale:grayscale(100%)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.\\\\[color-scheme\\\\:dark\\\\]{color-scheme:dark}.before\\\\:absolute:before{content:var(--tw-content);position:absolute}.before\\\\:inset-0:before{content:var(--tw-content);inset:0}.before\\\\:-translate-x-full:before{content:var(--tw-content);--tw-translate-x:-100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes shimmer{to{content:var(--tw-content);transform:translateX(100%)}}.before\\\\:animate-\\\\[shimmer_1\\\\.5s_infinite\\\\]:before{content:var(--tw-content);animation:shimmer 1.5s infinite}.before\\\\:bg-gradient-to-r:before{content:var(--tw-content);background-image:linear-gradient(to right,var(--tw-gradient-stops))}.before\\\\:from-transparent:before{content:var(--tw-content);--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.before\\\\:via-white\\\\/10:before{content:var(--tw-content);--tw-gradient-to:hsla(0,0%,100%,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),hsla(0,0%,100%,.1) var(--tw-gradient-via-position),var(--tw-gradient-to)}.before\\\\:to-transparent:before{content:var(--tw-content);--tw-gradient-to:transparent var(--tw-gradient-to-position)}@media (hover:hover) and (pointer:fine){.hover\\\\:bg-vercel-blue\\\\/90:hover{background-color:rgba(0,112,243,.9)}.hover\\\\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity))}.hover\\\\:text-gray-50:hover{--tw-text-opacity:1;color:rgb(250 250 250/var(--tw-text-opacity))}.hover\\\\:opacity-70:hover{opacity:.7}}.focus\\\\:border-vercel-pink:focus{--tw-border-opacity:1;border-color:rgb(255 0 128/var(--tw-border-opacity))}.focus\\\\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\\\\:ring-vercel-pink:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 0 128/var(--tw-ring-opacity))}.disabled\\\\:text-white\\\\/70:disabled{color:hsla(0,0%,100%,.7)}@media (hover:hover) and (pointer:fine){.group:hover .group-hover\\\\:text-gray-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity))}.group:hover .group-hover\\\\:text-vercel-cyan{--tw-text-opacity:1;color:rgb(80 227 194/var(--tw-text-opacity))}.group:hover .group-hover\\\\:opacity-80{opacity:.8}}@media (min-width:640px){.sm\\\\:block{display:block}}@media (min-width:768px){.md\\\\:order-1{order:1}.md\\\\:order-2{order:2}.md\\\\:order-3{order:3}.md\\\\:col-span-1{grid-column:span 1/span 1}.md\\\\:col-span-2{grid-column:span 2/span 2}}@media (min-width:1024px){.lg\\\\:static{position:static}.lg\\\\:bottom-0{bottom:0}.lg\\\\:z-auto{z-index:auto}.lg\\\\:col-span-1{grid-column:span 1/span 1}.lg\\\\:block{display:block}.lg\\\\:hidden{display:none}.lg\\\\:h-auto{height:auto}.lg\\\\:w-72{width:18rem}.lg\\\\:space-y-14>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(3.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3.5rem * var(--tw-space-y-reverse))}.lg\\\\:border-b-0{border-bottom-width:0}.lg\\\\:border-r{border-right-width:1px}.lg\\\\:border-gray-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity))}.lg\\\\:p-6{padding:1.5rem}.lg\\\\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\\\\:px-8{padding-left:2rem;padding-right:2rem}.lg\\\\:py-3{padding-top:.75rem;padding-bottom:.75rem}.lg\\\\:py-4{padding-top:1rem;padding-bottom:1rem}.lg\\\\:py-8{padding-top:2rem;padding-bottom:2rem}.lg\\\\:pl-72{padding-left:18rem}.lg\\\\:text-2xl{font-size:1.5rem;line-height:2rem}}\""
13 | );
14 |
--------------------------------------------------------------------------------