├── .eslintrc.json ├── README.md ├── public ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── next.config.js ├── app ├── head.tsx ├── layout.tsx ├── globals.css ├── page.tsx └── page.module.css ├── .gitignore ├── package.json ├── tsconfig.json ├── pages └── api │ └── articles │ ├── [slug] │ ├── comments │ │ └── index.ts │ └── index.ts │ └── index.ts ├── comments.json └── articles.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | see https://zenn.dev/azukiazusa/articles/next-js-app-dir-tutorial 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azukiazusa1/nextjs-app-dir-example/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | appDir: true, 5 | }, 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /app/head.tsx: -------------------------------------------------------------------------------- 1 | export default function Head() { 2 | return ( 3 | <> 4 | Create Next App 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | 3 | export default function RootLayout({ 4 | children, 5 | }: { 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 10 | {/* 11 | will contain the components returned by the nearest parent 12 | head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head 13 | */} 14 | 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-dir", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "13.1.1", 13 | "@types/node": "18.11.18", 14 | "@types/react": "18.0.26", 15 | "@types/react-dom": "18.0.10", 16 | "eslint": "8.31.0", 17 | "eslint-config-next": "13.1.1", 18 | "next": "13.1.1", 19 | "react": "18.2.0", 20 | "react-dom": "18.2.0", 21 | "typescript": "4.9.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /pages/api/articles/[slug]/comments/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import fs from "fs"; 3 | 4 | const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | const slug = req.query.slug; 11 | if (req.method === "GET") { 12 | await delay(3000); 13 | const comments = fs.readFileSync("comments.json", "utf8"); 14 | const articles = fs.readFileSync("articles.json", "utf8"); 15 | const articleId = JSON.parse(articles).articles.find( 16 | (a: any) => a.slug === slug 17 | ).id; 18 | const comment = JSON.parse(comments).comments.filter( 19 | (c: any) => c.articleId === articleId 20 | ); 21 | res.status(200).json(comment); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/api/articles/[slug]/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import fs from "fs"; 3 | 4 | const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); 5 | 6 | export default async function handler( 7 | req: NextApiRequest, 8 | res: NextApiResponse 9 | ) { 10 | const slug = req.query.slug; 11 | if (req.method === "GET") { 12 | await delay(1000); 13 | const articles = fs.readFileSync("articles.json", "utf8"); 14 | const article = JSON.parse(articles).articles.find( 15 | (a: any) => a.slug === slug 16 | ); 17 | if (!article) { 18 | res.status(404).end(); 19 | } 20 | 21 | res.status(200).json(article); 22 | } else if (req.method === "PUT") { 23 | delay(1000); 24 | const { title, content } = req.body; 25 | const articles = JSON.parse(fs.readFileSync("articles.json", "utf8")); 26 | const article = articles.find((a: any) => a.slug === slug); 27 | if (!article) { 28 | res.status(404).end(); 29 | } 30 | article.title = title; 31 | article.content = content; 32 | article.updatedAt = new Date(); 33 | fs.writeFileSync("articles.json", JSON.stringify(articles)); 34 | res.status(200).json(article); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /comments.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": [ 3 | { 4 | "id": 1, 5 | "body": "1つ目のコメントです。", 6 | "articleId": 1, 7 | "createdAt": "2019-01-01T00:00:00.000Z", 8 | "updatedAt": "2019-01-01T00:00:00.000Z", 9 | "author": { 10 | "name": "user1", 11 | "avatarUrl": "https://i.pravatar.cc/300?img=1" 12 | } 13 | }, 14 | { 15 | "id": 2, 16 | "body": "2つ目のコメントです。", 17 | "articleId": 1, 18 | "createdAt": "2019-01-01T00:00:00.000Z", 19 | "updatedAt": "2019-01-01T00:00:00.000Z", 20 | "author": { 21 | "name": "user2", 22 | "avatarUrl": "https://i.pravatar.cc/300?img=2" 23 | } 24 | }, 25 | { 26 | "id": 3, 27 | "body": "3つ目のコメントです。", 28 | "articleId": 1, 29 | "createdAt": "2019-01-01T00:00:00.000Z", 30 | "updatedAt": "2019-01-01T00:00:00.000Z", 31 | "author": { 32 | "name": "user3", 33 | "avatarUrl": "https://i.pravatar.cc/300?img=3" 34 | } 35 | }, 36 | { 37 | "id": 4, 38 | "body": "comment 4", 39 | "articleId": 3, 40 | "createdAt": "2019-01-01T00:00:00.000Z", 41 | "updatedAt": "2019-01-01T00:00:00.000Z", 42 | "author": { 43 | "name": "user4", 44 | "avatarUrl": "https://i.pravatar.cc/300?img=4" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/api/articles/index.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import fs from "fs"; 4 | import { randomUUID } from "crypto"; 5 | 6 | const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); 7 | 8 | export default async function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | if (req.method === "GET") { 13 | await delay(1500); 14 | const articles = JSON.parse(fs.readFileSync("articles.json", "utf8")); 15 | articles.articles.sort((a: any, b: any) => { 16 | return new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf(); 17 | }); 18 | res.status(200).json(articles); 19 | } else if (req.method === "POST") { 20 | await delay(1000); 21 | const { title, content } = req.body; 22 | const articles = JSON.parse(fs.readFileSync("articles.json", "utf8")); 23 | const id = articles.articles.length + 1; 24 | const date = new Date(); 25 | const slug = randomUUID(); 26 | const newArticle = { 27 | id, 28 | title, 29 | slug, 30 | content, 31 | createdAt: date, 32 | updatedAt: date, 33 | }; 34 | articles.articles.push(newArticle); 35 | fs.writeFileSync("articles.json", JSON.stringify(articles)); 36 | res.status(201).json(newArticle); 37 | } else { 38 | res.status(405).end(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /articles.json: -------------------------------------------------------------------------------- 1 | { 2 | "articles": [ 3 | { 4 | "id": 1, 5 | "title": "Article 1", 6 | "content": "吾輩は猫である。名前はまだ無い。どこで生れたかとんと見当がつかぬ。何でも薄暗いじめじめした所でニャーニャー泣いていた事だけは記憶している。吾輩はここで始めて人間というものを見た。しかもあとで聞くとそれは書生という人間中で一番獰悪な種族であったそうだ。この書生というのは時々我々を捕えて煮て食うという話である。しかしその当時は何という考もなかったから別段恐しいとも思わなかった。ただ彼の掌に載せられてスーと持ち上げられた時何だかフワフワした感じがあったばかりである。掌の上で少し落ちついて書生の顔を見たのがいわゆる人間というものの見始であろう。この時妙なものだと思った感じが今でも残っている。第一毛をもって装飾されべきはずの顔がつるつるしてまるで薬缶だ。その後猫にもだいぶ逢ったがこんな片輪には一度も出会わした事がない。のみならず顔の真中があまりに突起している。そうしてその穴の中から時々ぷうぷうと煙を", 7 | "slug": "38a88d63-7960-38f6-89d0-7eba192a53c0", 8 | "createdAt": "2019-01-03T00:00:00.000Z", 9 | "updatedAt": "2019-01-03T00:00:00.000Z" 10 | }, 11 | { 12 | "id": 2, 13 | "title": "Article 2", 14 | "content": "日本国民は、正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民との協和による成果と、わが国全土にわたつて自由のもたらす恵沢を確保し、政府の行為によつて再び戦争の惨禍が起ることのないやうにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は、国民の厳粛な信託によるものであつて、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基くものである。われらは、これに反する一切の憲法、法令及び詔勅を排除する。日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであつて、平和を愛する諸国民の公正と信義に信頼して、われらの安全と生存を保持しようと決意した。われらは、平和を維持し、専制と隷従、圧迫と偏狭を地上か", 15 | "slug": "1428cbaa-64fe-d260-e1a2-07e6c6d19e76", 16 | "createdAt": "2019-01-02T00:00:00", 17 | "updatedAt": "2019-01-02T00:00:00" 18 | }, 19 | { 20 | "id": 3, 21 | "title": "Article 3", 22 | "content": "木曾路はすべて山の中である。あるところは岨づたいに行く崖の道であり、あるところは数十間の深さに臨む木曾川の岸であり、あるところは山の尾をめぐる谷の入り口である。一筋の街道はこの深い森林地帯を貫いていた。東ざかいの桜沢から、西の十曲峠まで、木曾十一宿はこの街道に添うて、二十二里余にわたる長い谿谷の間に散在していた。道路の位置も幾たびか改まったもので、古道はいつのまにか深い山間に埋もれた。名高い桟も、蔦のかずらを頼みにしたような危い場処ではなくなって、徳川時代の末にはすでに渡ることのできる橋であった。新規に新規にとできた道はだんだん谷の下の方の位置へと降って来た。道の狭いところには、木を伐って並べ、藤づるでからめ、それで街道の狭いのを補った。長い間にこの木曾路に起こって来た変化は、いくらかずつでも嶮岨な山坂の多いところを歩きよくした。そのかわり、大雨ごとにやって来る河水の氾濫が旅行を困難にする", 23 | "slug": "7760be51-2afa-e855-9254-be3e15359bfb", 24 | "createdAt": "2019-01-01T00:00:00", 25 | "updatedAt": "2019-01-01T00:00:00" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { Inter } from '@next/font/google' 3 | import styles from './page.module.css' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export default function Home() { 8 | return ( 9 |
10 |
11 |

12 | Get started by editing  13 | app/page.tsx 14 |

15 |
16 | 21 | By{' '} 22 | Vercel Logo 30 | 31 |
32 |
33 | 34 |
35 | Next.js Logo 43 |
44 | 13 45 |
46 |
47 | 48 |
49 | 55 |

56 | Docs -> 57 |

58 |

59 | Find in-depth information about Next.js features and API. 60 |

61 |
62 | 63 | 69 |

70 | Templates -> 71 |

72 |

Explore the Next.js 13 playground.

73 |
74 | 75 | 81 |

82 | Deploy -> 83 |

84 |

85 | Instantly deploy your Next.js site to a shareable URL with Vercel. 86 |

87 |
88 |
89 |
90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(3, minmax(33%, auto)); 45 | width: var(--max-width); 46 | max-width: 100%; 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 34ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo, 108 | .thirteen { 109 | position: relative; 110 | } 111 | 112 | .thirteen { 113 | display: flex; 114 | justify-content: center; 115 | align-items: center; 116 | width: 75px; 117 | height: 75px; 118 | padding: 25px 10px; 119 | margin-left: 16px; 120 | transform: translateZ(0); 121 | border-radius: var(--border-radius); 122 | overflow: hidden; 123 | box-shadow: 0px 2px 8px -1px #0000001a; 124 | } 125 | 126 | .thirteen::before, 127 | .thirteen::after { 128 | content: ''; 129 | position: absolute; 130 | z-index: -1; 131 | } 132 | 133 | /* Conic Gradient Animation */ 134 | .thirteen::before { 135 | animation: 6s rotate linear infinite; 136 | width: 200%; 137 | height: 200%; 138 | background: var(--tile-border); 139 | } 140 | 141 | /* Inner Square */ 142 | .thirteen::after { 143 | inset: 0; 144 | padding: 1px; 145 | border-radius: var(--border-radius); 146 | background: linear-gradient( 147 | to bottom right, 148 | rgba(var(--tile-start-rgb), 1), 149 | rgba(var(--tile-end-rgb), 1) 150 | ); 151 | background-clip: content-box; 152 | } 153 | 154 | /* Enable hover only on non-touch devices */ 155 | @media (hover: hover) and (pointer: fine) { 156 | .card:hover { 157 | background: rgba(var(--card-rgb), 0.1); 158 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 159 | } 160 | 161 | .card:hover span { 162 | transform: translateX(4px); 163 | } 164 | } 165 | 166 | @media (prefers-reduced-motion) { 167 | .thirteen::before { 168 | animation: none; 169 | } 170 | 171 | .card:hover span { 172 | transform: none; 173 | } 174 | } 175 | 176 | /* Mobile and Tablet */ 177 | @media (max-width: 1023px) { 178 | .content { 179 | padding: 4rem; 180 | } 181 | 182 | .grid { 183 | grid-template-columns: 1fr; 184 | margin-bottom: 120px; 185 | max-width: 320px; 186 | text-align: center; 187 | } 188 | 189 | .card { 190 | padding: 1rem 2.5rem; 191 | } 192 | 193 | .card h2 { 194 | margin-bottom: 0.5rem; 195 | } 196 | 197 | .center { 198 | padding: 8rem 0 6rem; 199 | } 200 | 201 | .center::before { 202 | transform: none; 203 | height: 300px; 204 | } 205 | 206 | .description { 207 | font-size: 0.8rem; 208 | } 209 | 210 | .description a { 211 | padding: 1rem; 212 | } 213 | 214 | .description p, 215 | .description div { 216 | display: flex; 217 | justify-content: center; 218 | position: fixed; 219 | width: 100%; 220 | } 221 | 222 | .description p { 223 | align-items: center; 224 | inset: 0 0 auto; 225 | padding: 2rem 1rem 1.4rem; 226 | border-radius: 0; 227 | border: none; 228 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 229 | background: linear-gradient( 230 | to bottom, 231 | rgba(var(--background-start-rgb), 1), 232 | rgba(var(--callout-rgb), 0.5) 233 | ); 234 | background-clip: padding-box; 235 | backdrop-filter: blur(24px); 236 | } 237 | 238 | .description div { 239 | align-items: flex-end; 240 | pointer-events: none; 241 | inset: auto 0 0; 242 | padding: 2rem; 243 | height: 200px; 244 | background: linear-gradient( 245 | to bottom, 246 | transparent 0%, 247 | rgb(var(--background-end-rgb)) 40% 248 | ); 249 | z-index: 1; 250 | } 251 | } 252 | 253 | @media (prefers-color-scheme: dark) { 254 | .vercelLogo { 255 | filter: invert(1); 256 | } 257 | 258 | .logo, 259 | .thirteen img { 260 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 261 | } 262 | } 263 | 264 | @keyframes rotate { 265 | from { 266 | transform: rotate(360deg); 267 | } 268 | to { 269 | transform: rotate(0deg); 270 | } 271 | } 272 | --------------------------------------------------------------------------------