diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..e702518 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,61 @@ +name: Publish GitHub Pages + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy: + if: github.event_name != 'pull_request' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index ce89292..9a55258 100644 --- a/.gitignore +++ b/.gitignore @@ -256,6 +256,10 @@ ClientBin/ *.publishsettings orleans.codegen.cs +# Node-based site build +node_modules/ +dist/ + # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2aaa798 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,126 @@ +{ + "name": "tps-site", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tps-site", + "version": "1.0.0", + "dependencies": { + "highlight.js": "^11.11.1", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT", + "peer": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-anchor": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.0.tgz", + "integrity": "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==", + "license": "Unlicense", + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..865c6a8 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "tps-site", + "version": "1.0.0", + "private": true, + "description": "Build pipeline for the TPS documentation site.", + "type": "module", + "scripts": { + "build": "node scripts/build-site.mjs" + }, + "dependencies": { + "highlight.js": "^11.11.1", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0" + } +} diff --git a/public/CNAME b/public/CNAME new file mode 100644 index 0000000..815082c --- /dev/null +++ b/public/CNAME @@ -0,0 +1 @@ +tps.managed-code.com diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..077c9ae --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs new file mode 100644 index 0000000..4c940ef --- /dev/null +++ b/scripts/build-site.mjs @@ -0,0 +1,218 @@ +import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import hljs from "highlight.js"; +import MarkdownIt from "markdown-it"; +import markdownItAnchor from "markdown-it-anchor"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(__dirname, ".."); +const readmePath = path.join(rootDir, "README.md"); +const stylesPath = path.join(rootDir, "website", "site.css"); +const publicDir = path.join(rootDir, "public"); +const distDir = path.join(rootDir, "dist"); + +const readme = await readFile(readmePath, "utf8"); +const styles = await readFile(stylesPath, "utf8"); + +const md = new MarkdownIt({ + html: false, + linkify: true, + typographer: true, + highlight(code, language) { + if (language && hljs.getLanguage(language)) { + return `
${hljs.highlight(code, { language }).value}
`; + } + + return `
${md.utils.escapeHtml(code)}
`; + } +}).use(markdownItAnchor, { + slugify: slugifyHeading, + permalink: markdownItAnchor.permalink.linkInsideHeader({ + symbol: "#", + placement: "after", + ariaHidden: true + }) +}); + +const tokens = md.parse(readme, {}); +const title = extractTitle(tokens) ?? "TPS Format Specification"; +const summary = extractSummary(tokens) ?? "Markdown-based teleprompter scripts with timing, pacing, emotion, and styling metadata."; +const sections = extractSections(tokens); +const stats = buildStats(readme, sections); +const articleHtml = md.render(readme); +const builtAt = new Intl.DateTimeFormat("en", { + dateStyle: "long", + timeStyle: "short", + timeZone: "UTC" +}).format(new Date()); + +await rm(distDir, { recursive: true, force: true }); +await mkdir(distDir, { recursive: true }); +await cp(publicDir, distDir, { recursive: true }); + +const page = ` + + + + + ${escapeHtml(title)} + + + + + + + + + + +
+
+
+ ManagedCode Format Spec +

${escapeHtml(title)}

+

${escapeHtml(summary)}

+ +
+ +
+ +
+ + +
+ +
+ ${articleHtml} +
+
+
+
+ +`; + +await writeFile(path.join(distDir, "index.html"), page, "utf8"); + +function slugifyHeading(value) { + return value + .trim() + .toLowerCase() + .replace(/[`~!@#$%^&*()+={}\[\]|\\:;"'<>,.?/]/g, "") + .replace(/\s+/g, "-"); +} + +function extractTitle(tokenList) { + for (let index = 0; index < tokenList.length; index += 1) { + if (tokenList[index].type === "heading_open" && tokenList[index].tag === "h1") { + return tokenList[index + 1]?.content ?? null; + } + } + + return null; +} + +function extractSummary(tokenList) { + for (let index = 0; index < tokenList.length; index += 1) { + if (tokenList[index].type !== "paragraph_open") { + continue; + } + + const content = tokenList[index + 1]?.content?.trim(); + if (!content) { + continue; + } + + return content; + } + + return null; +} + +function extractSections(tokenList) { + const sectionList = []; + + for (let index = 0; index < tokenList.length; index += 1) { + const token = tokenList[index]; + if (token.type !== "heading_open") { + continue; + } + + if (token.tag !== "h2" && token.tag !== "h3") { + continue; + } + + const titleToken = tokenList[index + 1]; + const content = titleToken?.content?.trim(); + if (!content) { + continue; + } + + sectionList.push({ + depth: Number.parseInt(token.tag.replace("h", ""), 10), + title: content, + slug: slugifyHeading(content) + }); + } + + return sectionList; +} + +function buildStats(markdown, sectionList) { + const textOnly = markdown + .replace(/```[\s\S]*?```/g, " ") + .replace(/`[^`]+`/g, " ") + .replace(/\[[^\]]+\]\([^)]+\)/g, " ") + .replace(/[#>*_\-\n\r|]/g, " "); + + const words = textOnly.match(/\b[\p{L}\p{N}'-]+\b/gu) ?? []; + const segments = sectionList.filter((section) => section.depth === 2).length; + const subSections = sectionList.filter((section) => section.depth === 3).length; + + return [ + { label: "Sections", value: String(segments) }, + { label: "Subsections", value: String(subSections) }, + { label: "Words", value: new Intl.NumberFormat("en").format(words.length) } + ]; +} + +function renderSections(sectionList) { + return sectionList + .map((section) => { + const className = section.depth === 3 ? "toc-item toc-subitem" : "toc-item"; + return `
  • ${escapeHtml(section.title)}
  • `; + }) + .join(""); +} + +function escapeHtml(value) { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} diff --git a/website/site.css b/website/site.css new file mode 100644 index 0000000..6723a31 --- /dev/null +++ b/website/site.css @@ -0,0 +1,488 @@ +@import url("https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;600;700&family=Space+Grotesk:wght@500;700&display=swap"); + +:root { + color-scheme: dark; + --bg: #09111f; + --bg-elevated: rgba(10, 19, 34, 0.88); + --panel: rgba(16, 28, 48, 0.82); + --panel-strong: rgba(10, 18, 32, 0.96); + --border: rgba(148, 163, 184, 0.16); + --text: #edf4ff; + --muted: #9fb3cc; + --accent: #7dd3fc; + --accent-strong: #22d3ee; + --accent-soft: rgba(125, 211, 252, 0.14); + --warm: #f6c26b; + --code-bg: rgba(3, 7, 18, 0.88); + --shadow: 0 24px 80px rgba(2, 6, 23, 0.4); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + font-family: "Source Serif 4", "Iowan Old Style", "Palatino Linotype", serif; + background: + radial-gradient(circle at top left, rgba(34, 211, 238, 0.18), transparent 24rem), + radial-gradient(circle at top right, rgba(246, 194, 107, 0.2), transparent 22rem), + linear-gradient(180deg, #0b1324 0%, #08111d 46%, #060c16 100%); + color: var(--text); +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px); + background-size: 32px 32px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.5), transparent 92%); +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + color: #c4f0ff; +} + +code, +pre, +.button, +.eyebrow, +.panel-label, +.content-meta, +.toc-list a { + font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif; +} + +code { + padding: 0.15rem 0.45rem; + border-radius: 999px; + background: rgba(125, 211, 252, 0.1); + color: #dff8ff; + font-size: 0.9em; +} + +pre code { + display: block; + padding: 0; + background: transparent; + border-radius: 0; + color: inherit; + overflow-x: auto; +} + +.page-shell { + width: min(1360px, calc(100vw - 2rem)); + margin: 0 auto; + padding: 2rem 0 4rem; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(320px, 0.9fr); + gap: 1.5rem; + align-items: stretch; +} + +.hero-copy, +.hero-panel, +.toc-card, +.content-card { + backdrop-filter: blur(18px); + background: var(--bg-elevated); + border: 1px solid var(--border); + border-radius: 28px; + box-shadow: var(--shadow); +} + +.hero-copy { + padding: 3rem; +} + +.hero-panel { + padding: 2rem; + background: linear-gradient(180deg, rgba(13, 23, 39, 0.9), rgba(9, 17, 31, 0.92)); +} + +.eyebrow { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.45rem 0.85rem; + border-radius: 999px; + background: var(--accent-soft); + color: var(--accent); + font-size: 0.78rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.hero h1 { + margin: 1rem 0 0.75rem; + font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif; + font-size: clamp(2.6rem, 5vw, 4.8rem); + line-height: 0.95; + letter-spacing: -0.05em; +} + +.hero-summary { + max-width: 34rem; + margin: 0; + color: var(--muted); + font-size: 1.2rem; + line-height: 1.7; +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.9rem; + margin-top: 2rem; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0.9rem 1.25rem; + border-radius: 999px; + font-size: 0.95rem; + font-weight: 700; + letter-spacing: 0.01em; + transition: + transform 150ms ease, + border-color 150ms ease, + background-color 150ms ease; +} + +.button:hover { + transform: translateY(-1px); +} + +.button-primary { + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); + color: #062033; +} + +.button-secondary { + border: 1px solid rgba(125, 211, 252, 0.24); + background: rgba(8, 17, 29, 0.68); + color: var(--text); +} + +.panel-label { + margin: 0 0 1rem; + color: var(--warm); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.9rem; + margin: 0; +} + +.stats-grid div { + padding: 1rem; + border-radius: 20px; + background: rgba(125, 211, 252, 0.08); + border: 1px solid rgba(125, 211, 252, 0.12); +} + +.stats-grid dt { + margin: 0; + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.stats-grid dd { + margin: 0.55rem 0 0; + font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif; + font-size: 1.8rem; + font-weight: 700; +} + +.panel-note { + margin: 1.2rem 0 0; + color: var(--muted); + line-height: 1.7; +} + +.layout { + display: grid; + grid-template-columns: minmax(250px, 290px) minmax(0, 1fr); + gap: 1.5rem; + margin-top: 1.5rem; + align-items: start; +} + +.toc-card { + position: sticky; + top: 1rem; + padding: 1.25rem; + background: var(--panel); +} + +.toc-header, +.content-meta { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + align-items: center; + justify-content: space-between; +} + +.toc-header { + margin-bottom: 1rem; +} + +.toc-list { + margin: 0; + padding: 0; + list-style: none; +} + +.toc-item + .toc-item { + margin-top: 0.5rem; +} + +.toc-subitem { + padding-left: 1rem; +} + +.toc-list a { + display: block; + padding: 0.55rem 0.7rem; + border-radius: 14px; + color: var(--muted); + font-size: 0.95rem; + line-height: 1.35; +} + +.toc-list a:hover { + background: rgba(125, 211, 252, 0.08); + color: var(--text); +} + +.content-card { + padding: 1.5rem; + background: linear-gradient(180deg, rgba(11, 20, 36, 0.88), rgba(8, 15, 26, 0.96)); +} + +.content-meta { + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border); + color: var(--muted); + font-size: 0.85rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.markdown-body { + color: var(--text); + font-size: 1.1rem; + line-height: 1.82; +} + +.markdown-body > *:first-child { + margin-top: 0; +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3, +.markdown-body h4 { + position: relative; + margin: 2.4rem 0 0.85rem; + font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif; + line-height: 1.08; + letter-spacing: -0.04em; +} + +.markdown-body h1 { + font-size: 2.4rem; +} + +.markdown-body h2 { + font-size: 1.9rem; + color: #f3f8ff; +} + +.markdown-body h3 { + font-size: 1.4rem; + color: #d5e9ff; +} + +.markdown-body h4 { + font-size: 1.15rem; +} + +.markdown-body p, +.markdown-body ul, +.markdown-body ol, +.markdown-body table, +.markdown-body blockquote, +.markdown-body pre { + margin: 1rem 0; +} + +.markdown-body ul, +.markdown-body ol { + padding-left: 1.25rem; +} + +.markdown-body li + li { + margin-top: 0.35rem; +} + +.markdown-body strong { + color: #ffffff; +} + +.markdown-body hr { + border: 0; + height: 1px; + margin: 2rem 0; + background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.5), transparent); +} + +.markdown-body blockquote { + padding: 1rem 1.2rem; + border-left: 4px solid var(--warm); + border-radius: 18px; + background: rgba(246, 194, 107, 0.08); + color: #fff3dd; +} + +.markdown-body table { + width: 100%; + border-collapse: collapse; + overflow: hidden; + border-radius: 22px; + border: 1px solid rgba(148, 163, 184, 0.16); +} + +.markdown-body th, +.markdown-body td { + padding: 0.9rem 1rem; + text-align: left; + vertical-align: top; + border-bottom: 1px solid rgba(148, 163, 184, 0.12); +} + +.markdown-body th { + background: rgba(125, 211, 252, 0.08); + font-family: "Space Grotesk", "Avenir Next", "Segoe UI", sans-serif; + font-size: 0.86rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.markdown-body tr:last-child td { + border-bottom: 0; +} + +.markdown-body pre { + padding: 1.2rem 1.3rem; + overflow-x: auto; + border-radius: 22px; + background: var(--code-bg); + border: 1px solid rgba(125, 211, 252, 0.1); +} + +.markdown-body .hljs { + color: #d9f1ff; +} + +.markdown-body img { + max-width: 100%; + border-radius: 22px; +} + +.markdown-body .header-anchor { + margin-left: 0.4rem; + color: rgba(125, 211, 252, 0.65); + opacity: 0; + transition: opacity 150ms ease; +} + +.markdown-body h1:hover .header-anchor, +.markdown-body h2:hover .header-anchor, +.markdown-body h3:hover .header-anchor, +.markdown-body h4:hover .header-anchor { + opacity: 1; +} + +@media (max-width: 1040px) { + .hero, + .layout { + grid-template-columns: 1fr; + } + + .toc-card { + position: static; + } + + .stats-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 760px) { + .page-shell { + width: min(100vw - 1rem, 100%); + padding-top: 0.5rem; + } + + .hero-copy, + .hero-panel, + .toc-card, + .content-card { + border-radius: 24px; + } + + .hero-copy, + .hero-panel, + .content-card { + padding: 1.25rem; + } + + .hero h1 { + font-size: clamp(2.2rem, 12vw, 3.3rem); + } + + .hero-summary, + .markdown-body { + font-size: 1rem; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .content-meta, + .toc-header { + align-items: flex-start; + } +}