[READ-ONLY] Mirror of https://github.com/andrioid/gastro. Server Side templates, combining Go (frontmatter) and html/template (body) with full LSP support gastro.andri.dk
golang lsp-server template-engine
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(examples/gastro): migrate website to Tailwind v4

Rewrite every .gastro template from the hand-rolled BEM-ish
styles.css to a Tailwind v4 utility-first build. The compiled
`static/styles.css` is regenerated by a //go:generate directive
and stays committed so the Dockerfile and `gastro generate`
keep working without a Node toolchain at build time.

Tooling
- Pin `github:tailwindlabs/tailwindcss = v4.3.0` in mise.toml
(standalone binary, no Node runtime).
- Add `examples/gastro/generate.go` with two //go:generate
directives: tailwindcss build, then `gastro generate`. One
`go generate ./...` rebuilds both surfaces in source order.
- Add `scripts/dev/example-website` mise task for the hot-reload
loop (`gastro watch --build tailwindcss --build go-build --run`).

CSS source
- New `examples/gastro/tailwind.css` is the source of truth.
@theme ports the palette + font + radius tokens 1:1 from the
old :root block (so utilities like bg-accent / max-w-site /
font-display work directly). `--breakpoint-sm` and
`--breakpoint-md` are overridden to 600px / 900px so templates
use plain sm:/md: modifiers without arbitrary media queries.
- @layer components keeps a small set of unavoidable patterns:
.btn family, .code-block, .feature-card, .docs-nav family,
.spinner, .htmx-indicator (toggled by htmx.js at runtime),
.alert-success/.alert-error, mermaid hide-during-init, and
.prose-md (hand-rolled markdown styling — no
@tailwindcss/typography plugin, per AGENTS.md 3rd-party policy).
- Markdown-rendered docs content is wrapped in `prose-md` by
docs-layout.gastro, scoping the prose styles cleanly.

Templates
- All 6 components and 4 leaf pages rewritten to utilities. The
8 docs/*.gastro pages were untouched — they only use
.docs-nav* which stayed as a component class.
- Mermaid theme JS in components/layout.gastro hard-codes the
same hex literals declared in @theme; the sync comment now
points at tailwind.css @theme instead of :root.

CI gate
- `go generate ./... && git diff --exit-code` in
examples/gastro/, added as a step in the existing `test` job.
Catches drift in static/styles.css. Verified reproducible.

Docs
- docs/contributing.md (hardlinked into examples/gastro/docs/)
documents the rebuild command and the mise dev task.

Verification
- mise run lint, mise run test (-race), and the new CI gate all
pass clean. Browser checks via playwright-cli: home, docs/sse,
all three guestbook examples, mobile breakpoint, prev/next nav.
Zero console errors. Datastar SSE search-as-you-type filters
the entry list correctly.

+826 -1491
+10
.github/workflows/ci.yml
··· 13 13 - uses: actions/checkout@v4 14 14 - uses: jdx/mise-action@v4 15 15 - run: mise run test 16 + # Drift gate for examples/gastro: runs the //go:generate 17 + # directives in examples/gastro/generate.go (tailwindcss build + 18 + # gastro generate), then fails if any committed file changed. 19 + # In practice this catches static/styles.css drift; .gastro/ is 20 + # gitignored and isn't part of the committed surface. 21 + - name: Verify examples/gastro is up to date 22 + working-directory: examples/gastro 23 + run: | 24 + go generate ./... 25 + git diff --exit-code 16 26 17 27 lint: 18 28 runs-on: ubuntu-latest
+4
.gitignore
··· 20 20 21 21 # Node.js 22 22 node_modules/ 23 + # Tailwind contributor-local — only relevant if a contributor opts to 24 + # install tailwindcss via npm instead of the mise-managed standalone 25 + # binary. The committed examples/gastro/static/styles.css is what ships. 26 + examples/gastro/node_modules/ 23 27 24 28 # OpenCode (auto-managed) 25 29 .opencode/node_modules/
+30
docs/contributing.md
··· 52 52 `PATH` to develop in them; `go tool gastro <cmd>` is the canonical 53 53 invocation for example work. 54 54 55 + ### Working on `examples/gastro/` (the website) 56 + 57 + The website's CSS is compiled by Tailwind v4 from `tailwind.css` into 58 + `static/styles.css`. The compiled stylesheet is committed so the 59 + Dockerfile and `gastro generate` can consume it without a Node toolchain. 60 + 61 + Rebuild both the CSS and the `.gastro/` codegen tree in one step: 62 + 63 + ```sh 64 + cd examples/gastro 65 + go generate ./... 66 + ``` 67 + 68 + The `//go:generate` directives in `examples/gastro/generate.go` invoke 69 + `tailwindcss` followed by `gastro generate`. CI runs the same command 70 + and fails if the result differs from what's committed. 71 + 72 + For a hot-reload dev loop that rebuilds CSS, regenerates the gastro 73 + tree, and restarts the binary on every change: 74 + 75 + ```sh 76 + mise run dev:example-website 77 + ``` 78 + 79 + The `tailwindcss` binary is provided by the `mise` install 80 + (`github:tailwindlabs/tailwindcss` in `mise.toml`). If you don't use 81 + mise, install Tailwind v4 yourself — the standalone binary, or 82 + `npm i -g @tailwindcss/cli@4` both work as long as `tailwindcss` is on 83 + your `PATH`. 84 + 55 85 ### Linting 56 86 57 87 ```sh
+31 -22
examples/gastro/components/docs-layout.gastro
··· 9 9 p := gastro.Props() 10 10 Title := p.Title 11 11 Active := p.Active 12 + 13 + // Active-link class chunk used across every nav <li>. Conditional in 14 + // each entry below via `{{ if eq .Active "..." }}`. Pulled out as a 15 + // constant so the template doesn't repeat the literal string 11 times. 16 + ActiveLink := "bg-accent-light text-accent font-semibold" 12 17 --- 13 18 {{ wrap Layout (dict "Title" .Title) }} 14 - <div class="docs-container"> 15 - <aside class="docs-sidebar"> 16 - <details class="docs-sidebar-menu" open> 17 - <summary class="docs-sidebar-toggle">Navigation</summary> 18 - <nav class="docs-sidebar-nav" aria-label="Documentation"> 19 - <h3>Documentation</h3> 20 - <ul> 21 - <li><a href="/docs/getting-started" {{ if eq .Active "getting-started" }}class="active" aria-current="page"{{ end }}>Getting Started (new project)</a></li> 22 - <li><a href="/docs/getting-started-library" {{ if eq .Active "getting-started-library" }}class="active" aria-current="page"{{ end }}>Getting Started (existing project)</a></li> 23 - <li><a href="/docs/pages" {{ if eq .Active "pages" }}class="active" aria-current="page"{{ end }}>Pages &amp; Routing</a></li> 24 - <li><a href="/docs/components" {{ if eq .Active "components" }}class="active" aria-current="page"{{ end }}>Components</a></li> 25 - <li><a href="/docs/sse" {{ if eq .Active "sse" }}class="active" aria-current="page"{{ end }}>SSE &amp; Datastar</a></li> 26 - <li><a href="/docs/helpers" {{ if eq .Active "helpers" }}class="active" aria-current="page"{{ end }}>Template Helpers</a></li> 27 - <li><a href="/docs/deployment" {{ if eq .Active "deployment" }}class="active" aria-current="page"{{ end }}>Deployment</a></li> 28 - <li><a href="/docs/comparison" {{ if eq .Active "comparison" }}class="active" aria-current="page"{{ end }}>Comparison</a></li> 19 + <div class="max-w-site mx-auto px-6 py-6 grid grid-cols-1 gap-5 min-h-[calc(100vh-156px)] md:grid-cols-[210px_1fr] md:px-8 md:py-8 md:gap-12"> 20 + <aside class="md:sticky md:top-[72px] md:self-start md:pt-3 max-md:border-b max-md:border-border max-md:pb-0"> 21 + <details class="block" open> 22 + <summary data-disclosure class="hidden max-md:block py-[0.6rem] text-[0.88rem] font-semibold text-text cursor-pointer list-none [&::-webkit-details-marker]:hidden before:content-['\25B6'] before:inline-block before:mr-2 before:text-[0.65rem] before:transition-transform">Navigation</summary> 23 + <nav class="max-md:pb-4" aria-label="Documentation"> 24 + {{- /* Sidebar nav. The h3 + ul pair repeats per section. 25 + Active-link class chunk lives in `.ActiveLink`. */ -}} 26 + <h3 class="font-mono text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-text-subtle mb-[0.6rem] mt-0">Documentation</h3> 27 + <ul class="list-none p-0 m-0 [&_li]:mb-[0.15rem] [&_a]:block [&_a]:px-[0.7rem] [&_a]:py-[0.3rem] [&_a]:rounded-[5px] [&_a]:text-text-muted [&_a]:text-[0.88rem] [&_a]:font-medium [&_a]:transition-all [&_a:hover]:text-text [&_a:hover]:bg-surface"> 28 + <li><a href="/docs/getting-started" {{ if eq .Active "getting-started" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Getting Started (new project)</a></li> 29 + <li><a href="/docs/getting-started-library" {{ if eq .Active "getting-started-library" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Getting Started (existing project)</a></li> 30 + <li><a href="/docs/pages" {{ if eq .Active "pages" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Pages &amp; Routing</a></li> 31 + <li><a href="/docs/components" {{ if eq .Active "components" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Components</a></li> 32 + <li><a href="/docs/sse" {{ if eq .Active "sse" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>SSE &amp; Datastar</a></li> 33 + <li><a href="/docs/helpers" {{ if eq .Active "helpers" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Template Helpers</a></li> 34 + <li><a href="/docs/deployment" {{ if eq .Active "deployment" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Deployment</a></li> 35 + <li><a href="/docs/comparison" {{ if eq .Active "comparison" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Comparison</a></li> 29 36 </ul> 30 - <h3>Examples</h3> 31 - <ul> 32 - <li><a href="/examples/guestbook-plain" {{ if eq .Active "guestbook-plain" }}class="active" aria-current="page"{{ end }}>Guestbook (Plain)</a></li> 33 - <li><a href="/examples/guestbook-datastar" {{ if eq .Active "guestbook-datastar" }}class="active" aria-current="page"{{ end }}>Guestbook (Datastar)</a></li> 34 - <li><a href="/examples/guestbook-htmx" {{ if eq .Active "guestbook-htmx" }}class="active" aria-current="page"{{ end }}>Guestbook (HTMX)</a></li> 37 + <h3 class="font-mono text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-text-subtle mb-[0.6rem] mt-6">Examples</h3> 38 + <ul class="list-none p-0 m-0 [&_li]:mb-[0.15rem] [&_a]:block [&_a]:px-[0.7rem] [&_a]:py-[0.3rem] [&_a]:rounded-[5px] [&_a]:text-text-muted [&_a]:text-[0.88rem] [&_a]:font-medium [&_a]:transition-all [&_a:hover]:text-text [&_a:hover]:bg-surface"> 39 + <li><a href="/examples/guestbook-plain" {{ if eq .Active "guestbook-plain" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Guestbook (Plain)</a></li> 40 + <li><a href="/examples/guestbook-datastar" {{ if eq .Active "guestbook-datastar" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Guestbook (Datastar)</a></li> 41 + <li><a href="/examples/guestbook-htmx" {{ if eq .Active "guestbook-htmx" }}class="{{ .ActiveLink }}" aria-current="page"{{ end }}>Guestbook (HTMX)</a></li> 35 42 </ul> 36 43 </nav> 37 44 </details> 38 45 <script> 46 + // Auto-close the sidebar accordion on small viewports so 47 + // the docs content is the first thing the user sees. 39 48 if (window.matchMedia("(max-width: 900px)").matches) { 40 - document.querySelector(".docs-sidebar-menu").removeAttribute("open"); 49 + document.querySelector("aside details").removeAttribute("open"); 41 50 } 42 51 </script> 43 52 </aside> 44 - <main id="main-content" class="docs-content"> 53 + <main id="main-content" class="prose-md pt-3 pb-16"> 45 54 {{ .Children }} 46 55 </main> 47 56 </div>
+14 -7
examples/gastro/components/guestbook-entry-edit.gastro
··· 17 17 HtmxSaveURL := fmt.Sprintf("/api/htmx/save/%s", p.ID) 18 18 HtmxTarget := fmt.Sprintf("#entry-%s", p.ID) 19 19 EditMessageID := fmt.Sprintf("edit-message-%s", p.ID) 20 + 21 + // Mirror of the input chrome from guestbook-form.gastro. Inlined here 22 + // (rather than DRYing across components) so this component is 23 + // self-contained when rendered by SSE handlers. 24 + InputClass := "w-full px-[0.85rem] py-[0.6rem] border border-border rounded font-sans text-[0.9rem] text-text bg-bg transition-colors focus:outline-none focus:border-accent focus:ring-accent-soft placeholder:text-text-subtle" 20 25 --- 21 - <div id="entry-{{ .ID }}" class="guestbook-entry guestbook-entry--editing"> 22 - <div class="guestbook-entry-header"> 23 - <strong class="guestbook-entry-name">{{ .Name }}</strong> 26 + {{- /* Edit-mode variant of the entry row. The accent border + soft 27 + outer glow signal that this row is the actively-edited one. */ -}} 28 + <div id="entry-{{ .ID }}" class="relative bg-bg border border-accent rounded p-[0.85rem_1rem] ring-accent-soft-light"> 29 + <div class="flex justify-between items-baseline mb-[0.35rem] gap-2"> 30 + <strong class="text-[0.9rem] font-semibold text-text">{{ .Name }}</strong> 24 31 </div> 25 32 {{ if eq .Mode "ds" }} 26 - <textarea data-bind:message class="guestbook-input guestbook-textarea" aria-label="Edit message">{{ .Message }}</textarea> 27 - <div class="guestbook-edit-actions"> 33 + <textarea data-bind:message class="{{ .InputClass }} min-h-[80px] resize-y leading-[1.5]" aria-label="Edit message">{{ .Message }}</textarea> 34 + <div class="flex gap-2 mt-[0.6rem]"> 28 35 <button type="button" class="btn btn-primary btn-sm" data-on:click="{{ .DsSaveURL | safeJS }}" aria-label="Save changes">Save</button> 29 36 </div> 30 37 {{ end }} 31 38 {{ if eq .Mode "htmx" }} 32 39 <form> 33 - <textarea id="{{ .EditMessageID }}" name="message" class="guestbook-input guestbook-textarea" aria-label="Edit message">{{ .Message }}</textarea> 34 - <div class="guestbook-edit-actions"> 40 + <textarea id="{{ .EditMessageID }}" name="message" class="{{ .InputClass }} min-h-[80px] resize-y leading-[1.5]" aria-label="Edit message">{{ .Message }}</textarea> 41 + <div class="flex gap-2 mt-[0.6rem]"> 35 42 <button type="button" class="btn btn-primary btn-sm" hx-post="{{ .HtmxSaveURL }}" hx-target="{{ .HtmxTarget }}" hx-swap="outerHTML" hx-include="#{{ .EditMessageID }}" aria-label="Save changes">Save</button> 36 43 </div> 37 44 </form>
+10 -7
examples/gastro/components/guestbook-entry.gastro
··· 19 19 HtmxEditURL := fmt.Sprintf("/api/htmx/edit/%s", p.ID) 20 20 HtmxTarget := fmt.Sprintf("#entry-%s", p.ID) 21 21 --- 22 - <div id="entry-{{ .ID }}" class="guestbook-entry"> 23 - <div class="guestbook-entry-header"> 24 - <strong class="guestbook-entry-name">{{ .Name }}</strong> 25 - <time class="guestbook-entry-time">{{ .Time }}</time> 22 + {{- /* Entry row. Hover reveals the edit button via the `group/entry` 23 + parent. The edit button is rendered conditionally based on Mode 24 + so plain-form pages don't ship a no-op button at all. */ -}} 25 + <div id="entry-{{ .ID }}" class="group/entry relative bg-bg border border-border rounded p-[0.85rem_1rem] transition-colors hover:border-accent"> 26 + <div class="flex justify-between items-baseline mb-[0.35rem] gap-2"> 27 + <strong class="text-[0.9rem] font-semibold text-text">{{ .Name }}</strong> 28 + <time class="font-mono text-[0.72rem] text-text-subtle whitespace-nowrap">{{ .Time }}</time> 26 29 </div> 27 - <p class="guestbook-entry-message">{{ .Message }}</p> 30 + <p class="text-[0.9rem] text-text-muted leading-[1.5] m-0">{{ .Message }}</p> 28 31 {{ if eq .Mode "ds" }} 29 - <button class="guestbook-edit-btn" data-on:click="{{ .DsEditURL | safeJS }}" aria-label="Edit entry">Edit</button> 32 + <button class="absolute top-[0.6rem] right-[0.6rem] bg-transparent border border-border rounded-[4px] px-2 py-[0.2rem] font-mono text-[0.7rem] text-text-subtle cursor-pointer transition-all opacity-0 group-hover/entry:opacity-100 hover:text-accent hover:border-accent" data-on:click="{{ .DsEditURL | safeJS }}" aria-label="Edit entry">Edit</button> 30 33 {{ end }} 31 34 {{ if eq .Mode "htmx" }} 32 - <button class="guestbook-edit-btn" hx-get="{{ .HtmxEditURL }}" hx-target="{{ .HtmxTarget }}" hx-swap="outerHTML" aria-label="Edit entry">Edit</button> 35 + <button class="absolute top-[0.6rem] right-[0.6rem] bg-transparent border border-border rounded-[4px] px-2 py-[0.2rem] font-mono text-[0.7rem] text-text-subtle cursor-pointer transition-all opacity-0 group-hover/entry:opacity-100 hover:text-accent hover:border-accent" hx-get="{{ .HtmxEditURL }}" hx-target="{{ .HtmxTarget }}" hx-swap="outerHTML" aria-label="Edit entry">Edit</button> 33 36 {{ end }} 34 37 </div>
+11 -5
examples/gastro/components/guestbook-form.gastro
··· 7 7 p := gastro.Props() 8 8 Error := p.Error 9 9 Success := p.Success 10 + 11 + // Shared input chrome reused by name + textarea fields and by the 12 + // search input on the example pages. Hoisted to a frontmatter var so 13 + // the long utility chain only lives in one place; the codegen exposes 14 + // it to the template as `{{ .InputClass }}` automatically. 15 + InputClass := "w-full px-[0.85rem] py-[0.6rem] border border-border rounded font-sans text-[0.9rem] text-text bg-bg transition-colors focus:outline-none focus:border-accent focus:ring-accent-soft placeholder:text-text-subtle" 10 16 --- 11 17 <div id="guestbook-form-wrapper"> 12 18 {{ if .Success }} 13 - <div class="guestbook-alert guestbook-alert--success" role="status">{{ .Success }}</div> 19 + <div class="alert-success px-[0.85rem] py-[0.6rem] rounded text-[0.88rem] font-medium" role="status">{{ .Success }}</div> 14 20 {{ end }} 15 21 {{ if .Error }} 16 - <div class="guestbook-alert guestbook-alert--error" role="alert">{{ .Error }}</div> 22 + <div class="alert-error px-[0.85rem] py-[0.6rem] rounded text-[0.88rem] font-medium" role="alert">{{ .Error }}</div> 17 23 {{ end }} 18 - <div class="guestbook-form-fields"> 19 - <input id="guestbook-name" type="text" name="name" placeholder="Your name" class="guestbook-input" required aria-label="Your name" autocomplete="off"> 20 - <textarea id="guestbook-message" name="message" placeholder="Leave a message..." class="guestbook-input guestbook-textarea" required aria-label="Your message"></textarea> 24 + <div class="flex flex-col gap-[0.6rem]"> 25 + <input id="guestbook-name" type="text" name="name" placeholder="Your name" class="{{ .InputClass }}" required aria-label="Your name" autocomplete="off"> 26 + <textarea id="guestbook-message" name="message" placeholder="Leave a message..." class="{{ .InputClass }} min-h-[80px] resize-y leading-[1.5]" required aria-label="Your message"></textarea> 21 27 </div> 22 28 </div>
+15 -14
examples/gastro/components/hero.gastro
··· 8 8 p := gastro.Props() 9 9 IntroHero := p.IntroHero 10 10 --- 11 - <div class="hero-wrapper"> 12 - <section class="hero"> 13 - <div class="hero-content"> 14 - <p class="hero-eyebrow">A fresh take on Go web development</p> 15 - <h1 class="hero-title">Cook up web pages with <span class="hero-accent">Go</span></h1> 16 - <p class="hero-description">Combine Go frontmatter with html/template markup in a single <code>.gastro</code> file. The compiler generates type-safe code with automatic file-based routing.</p> 17 - <div class="hero-actions"> 11 + <div class="bg-hero-bg"> 12 + <section class="grid grid-cols-1 gap-8 max-w-site mx-auto px-6 py-10 items-center md:grid-cols-[1.1fr_0.9fr] md:gap-[clamp(2rem,4vw,5rem)] md:px-8 md:py-[clamp(3rem,6vw,6rem)]"> 13 + <div class="flex flex-col animate-[fade-up_0.5s_ease-out_both]"> 14 + <p class="font-mono text-[0.82rem] text-accent-warm tracking-[0.04em] mb-4 font-medium">A fresh take on Go web development</p> 15 + <h1 class="font-display font-bold leading-[1.15] text-header-text tracking-[-0.02em] mb-5 text-[clamp(1.75rem,5vw,2.2rem)] md:text-[clamp(2rem,4vw,3rem)]" 16 + >Cook up web pages with <span class="text-accent">Go</span></h1> 17 + <p class="text-text-subtle leading-[1.7] mb-8 max-w-[480px] text-base md:text-[clamp(1rem,1.5vw,1.1rem)]">Combine Go frontmatter with html/template markup in a single <code>.gastro</code> file. The compiler generates type-safe code with automatic file-based routing.</p> 18 + <div class="flex gap-3 flex-wrap max-sm:flex-col max-sm:items-stretch"> 18 19 <a href="/docs/getting-started" class="btn btn-primary">Get Started</a> 19 20 <a href="https://github.com/andrioid/gastro" class="btn btn-secondary" target="_blank" rel="noopener">GitHub<span class="sr-only"> (opens in new tab)</span></a> 20 21 </div> 21 22 </div> 22 - <div class="hero-code" aria-hidden="true"> 23 - <div class="code-window"> 24 - <div class="code-window-bar"> 25 - <span class="code-dot red"></span> 26 - <span class="code-dot yellow"></span> 27 - <span class="code-dot green"></span> 28 - <span class="code-window-title">components/greeting.gastro</span> 23 + <div class="min-w-0 max-md:max-w-[520px] animate-[fade-up_0.5s_ease-out_0.15s_both]" aria-hidden="true"> 24 + <div class="hero-code-window bg-code-bg rounded-lg overflow-hidden text-accent-light shadow-code-window"> 25 + <div class="flex items-center px-4 py-[0.7rem] bg-black/25 gap-2"> 26 + <span class="w-2.5 h-2.5 rounded-full bg-[#e85c4a]"></span> 27 + <span class="w-2.5 h-2.5 rounded-full bg-[#e8a73e]"></span> 28 + <span class="w-2.5 h-2.5 rounded-full bg-[#5eb86c]"></span> 29 + <span class="ml-2 text-[0.78rem] text-text-subtle font-mono">components/greeting.gastro</span> 29 30 </div> 30 31 {{ .IntroHero }} 31 32
+10 -10
examples/gastro/components/layout.gastro
··· 29 29 </head> 30 30 <body> 31 31 <a href="#main-content" class="skip-link">Skip to content</a> 32 - <header class="site-header"> 33 - <div class="nav-container"> 34 - <a href="/" class="nav-logo" aria-label="Gastro home"> 35 - <img src="/static/logo.svg" alt="Gastro" height="26" width="120"> 32 + <header class="sticky top-0 z-[100] bg-header-bg"> 33 + <div class="max-w-site mx-auto px-8 h-14 flex items-center justify-between"> 34 + <a href="/" class="flex items-center text-header-text" aria-label="Gastro home"> 35 + <img src="/static/logo.svg" alt="Gastro" height="26" width="120" class="h-[26px]"> 36 36 </a> 37 - <nav class="nav-links" aria-label="Main"> 37 + <nav class="flex items-center gap-7 text-sm font-medium tracking-[0.01em] [&_a]:text-text-subtle [&_a:hover]:text-header-text [&_a]:transition-colors" aria-label="Main"> 38 38 <a href="/docs/getting-started">Docs</a> 39 39 <a href="https://github.com/andrioid/gastro" target="_blank" rel="noopener">GitHub<span class="sr-only"> (opens in new tab)</span></a> 40 40 </nav> ··· 47 47 // mermaid (the library is ~600KB gzipped). 48 48 // 49 49 // The themeVariables block below maps the site's design tokens 50 - // (see static/styles.css :root) onto mermaid's `base` theme so 50 + // (see tailwind.css @theme) onto mermaid's `base` theme so 51 51 // diagrams pick up the same warm cream / muted brown / accent 52 52 // orange palette as the rest of the site instead of mermaid's 53 53 // default pastel-purple. Keep these in sync with --color-* ··· 98 98 .catch((err) => console.error('mermaid load failed', err)); 99 99 } 100 100 </script> 101 - <footer class="site-footer"> 102 - <div class="footer-container"> 103 - <p>Built with Gastro &mdash; a file-based component framework for Go.</p> 104 - <p><a href="https://github.com/andrioid/gastro">Source on GitHub</a></p> 101 + <footer class="border-t border-border bg-surface px-8 py-8 [&_a]:text-text-muted [&_a]:font-medium [&_a:hover]:text-accent"> 102 + <div class="max-w-site mx-auto text-center"> 103 + <p class="text-text-muted text-[0.85rem] mb-[0.2rem]">Built with Gastro &mdash; a file-based component framework for Go.</p> 104 + <p class="text-text-muted text-[0.85rem] mb-[0.2rem]"><a href="https://github.com/andrioid/gastro">Source on GitHub</a></p> 105 105 </div> 106 106 </footer> 107 107 </body>
+24
examples/gastro/generate.go
··· 1 + // Build-artifact rebuild directives for the gastro-website example. 2 + // 3 + // `go generate ./...` from this directory will: 4 + // 5 + // 1. Compile Tailwind CSS source (tailwind.css) into the embedded 6 + // stylesheet at static/styles.css. Output is minified for parity 7 + // with what ships in the binary. 8 + // 2. Run `gastro generate` to regenerate the .gastro/ package tree 9 + // against any .gastro template changes. 10 + // 11 + // `go generate` runs directives top-to-bottom within a single file, so 12 + // the freshly-built CSS is on disk before gastro generate's //go:embed 13 + // snapshot of static/ is taken. 14 + // 15 + // Both commands must be on PATH. mise satisfies this once contributors 16 + // run `mise install` from the repo root (see mise.toml). 17 + // 18 + // CI gate: `go generate ./... && git diff --exit-code` from this 19 + // directory catches drift in either the CSS or the .gastro/ tree. 20 + 21 + //go:generate tailwindcss -i tailwind.css -o static/styles.css --minify 22 + //go:generate gastro generate 23 + 24 + package main
+15 -9
examples/gastro/pages/examples/guestbook-datastar.gastro
··· 10 10 Title := "Guestbook (Datastar)" 11 11 Active := "guestbook-datastar" 12 12 Entries := demo.ListEntries() 13 + 14 + GuestbookContainer := "flex flex-col gap-5 bg-surface border border-border rounded-lg p-4 sm:p-[clamp(1.25rem,2.5vw,2rem)]" 15 + SearchInputClass := "w-full px-[0.85rem] py-[0.6rem] pr-10 border border-border rounded font-sans text-[0.9rem] text-text bg-bg transition-colors focus:outline-none focus:border-accent focus:ring-accent-soft placeholder:text-text-subtle" 13 16 --- 14 17 {{ wrap DocsLayout (dict "Title" .Title "Active" .Active) }} 15 18 <h1>Guestbook (Datastar)</h1> 16 19 <p>A working guestbook powered by <a href="https://data-star.dev/" target="_blank" rel="noopener">Datastar<span class="sr-only"> (opens in new tab)</span></a> and server-sent events. Search, add entries, and edit inline &mdash; all without page reloads.</p> 17 20 18 - <div class="demo-section"> 19 - <div class="guestbook-container"> 20 - <div class="guestbook-search-wrapper"> 21 + <div class="py-8"> 22 + <div class="{{ .GuestbookContainer }}"> 23 + <div class="relative"> 21 24 <input 22 25 type="search" 23 - class="guestbook-input guestbook-search" 26 + class="{{ .SearchInputClass }}" 24 27 placeholder="Search entries..." 25 28 aria-label="Search guestbook entries" 26 29 autocomplete="off" ··· 28 31 data-on:input__debounce.300ms="{{ `@get('/api/ds/search')` | safeJS }}" 29 32 data-indicator:_searching 30 33 > 31 - <div class="loading-indicator" data-show="$_searching" aria-hidden="true"> 34 + {{- /* Datastar-driven loading indicator. The wrapper is 35 + always in the DOM; data-show flips its visibility 36 + based on the `_searching` reactive signal. */ -}} 37 + <div class="absolute top-1/2 right-3 -translate-y-1/2" data-show="$_searching" aria-hidden="true"> 32 38 <div class="spinner"></div> 33 39 </div> 34 40 </div> 35 41 36 - <div id="guestbook-list" class="guestbook-list" role="list" aria-live="polite" aria-label="Guestbook entries"> 42 + <div id="guestbook-list" class="flex flex-col gap-3" role="list" aria-live="polite" aria-label="Guestbook entries"> 37 43 {{ range .Entries }} 38 44 {{ GuestbookEntry (dict "ID" .ID "Name" .Name "Message" .Message "Time" (.CreatedAt.Format "Jan 2, 3:04 PM") "Mode" "ds") }} 39 45 {{ end }} 40 46 </div> 41 47 42 - <form class="guestbook-form" 48 + <form class="flex flex-col gap-3" 43 49 data-on:submit="{{ `@post('/api/ds/add', {contentType: 'form'})` | safeJS }}" 44 50 data-indicator:_submitting 45 51 > 46 52 {{ GuestbookForm (dict "Error" "" "Success" "") }} 47 - <button type="submit" class="btn btn-primary guestbook-submit" data-attr:disabled="$_submitting"> 53 + <button type="submit" class="btn btn-primary self-start" data-attr:disabled="$_submitting"> 48 54 <span data-show="!$_submitting">Sign Guestbook</span> 49 55 <span data-show="$_submitting">Sending...</span> 50 56 </button> ··· 52 58 </div> 53 59 </div> 54 60 55 - <div class="demo-explanation"> 61 + <div class="mt-8"> 56 62 <h2>How It Works</h2> 57 63 <p>This demo showcases four common hypermedia patterns using Datastar and Gastro's SSE support.</p> 58 64
+18 -11
examples/gastro/pages/examples/guestbook-htmx.gastro
··· 10 10 Title := "Guestbook (HTMX)" 11 11 Active := "guestbook-htmx" 12 12 Entries := demo.ListEntries() 13 + 14 + GuestbookContainer := "flex flex-col gap-5 bg-surface border border-border rounded-lg p-4 sm:p-[clamp(1.25rem,2.5vw,2rem)]" 15 + SearchInputClass := "w-full px-[0.85rem] py-[0.6rem] pr-10 border border-border rounded font-sans text-[0.9rem] text-text bg-bg transition-colors focus:outline-none focus:border-accent focus:ring-accent-soft placeholder:text-text-subtle" 13 16 --- 14 17 {{ wrap DocsLayout (dict "Title" .Title "Active" .Active) }} 15 18 <h1>Guestbook (HTMX)</h1> ··· 17 20 18 21 <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script> 19 22 20 - <div class="demo-section"> 21 - <div class="guestbook-container"> 22 - <div class="guestbook-search-wrapper"> 23 + <div class="py-8"> 24 + <div class="{{ .GuestbookContainer }}"> 25 + <div class="relative"> 23 26 <input 24 27 type="search" 25 - class="guestbook-input guestbook-search" 28 + class="{{ .SearchInputClass }}" 26 29 name="q" 27 30 placeholder="Search entries..." 28 31 aria-label="Search guestbook entries" ··· 32 35 hx-target="#htmx-guestbook-list" 33 36 hx-indicator="#htmx-search-indicator" 34 37 > 35 - <div id="htmx-search-indicator" class="loading-indicator htmx-indicator" aria-hidden="true"> 38 + {{- /* `htmx-indicator` is the literal class htmx.js looks 39 + for; pair with absolute-positioned utilities for 40 + the spinner placement. The visibility flip lives in 41 + tailwind.css @layer components. */ -}} 42 + <div id="htmx-search-indicator" class="htmx-indicator absolute top-1/2 right-3 -translate-y-1/2" aria-hidden="true"> 36 43 <div class="spinner"></div> 37 44 </div> 38 45 </div> 39 46 40 - <div id="htmx-guestbook-list" class="guestbook-list" role="list" aria-live="polite" aria-label="Guestbook entries"> 47 + <div id="htmx-guestbook-list" class="flex flex-col gap-3" role="list" aria-live="polite" aria-label="Guestbook entries"> 41 48 {{ range .Entries }} 42 49 {{ GuestbookEntry (dict "ID" .ID "Name" .Name "Message" .Message "Time" (.CreatedAt.Format "Jan 2, 3:04 PM") "Mode" "htmx") }} 43 50 {{ end }} 44 51 </div> 45 52 46 - <form class="guestbook-form" 53 + <form class="flex flex-col gap-3" 47 54 hx-post="/api/htmx/add" 48 55 hx-target="#htmx-guestbook-list" 49 56 hx-swap="outerHTML" 50 57 hx-indicator="#htmx-submit-indicator" 51 58 > 52 59 {{ GuestbookForm (dict "Error" "" "Success" "") }} 53 - <div class="guestbook-submit-wrapper"> 54 - <button type="submit" class="btn btn-primary guestbook-submit">Sign Guestbook</button> 55 - <div id="htmx-submit-indicator" class="loading-indicator htmx-indicator" aria-hidden="true"> 60 + <div class="flex items-center gap-3"> 61 + <button type="submit" class="btn btn-primary self-start">Sign Guestbook</button> 62 + <div id="htmx-submit-indicator" class="htmx-indicator relative" aria-hidden="true"> 56 63 <div class="spinner"></div> 57 64 </div> 58 65 </div> ··· 60 67 </div> 61 68 </div> 62 69 63 - <div class="demo-explanation"> 70 + <div class="mt-8"> 64 71 <h2>How It Works</h2> 65 72 <p>HTMX extends HTML with attributes that issue AJAX requests and swap HTML fragments into the page. Unlike Datastar (which uses SSE), HTMX handlers return plain HTML responses.</p> 66 73
+12 -7
examples/gastro/pages/examples/guestbook-plain.gastro
··· 14 14 Entries := demo.ListEntries() 15 15 Error := q.Get("error") 16 16 Success := q.Get("success") 17 + 18 + // Outer container chrome shared by all three guestbook examples. 19 + // Hoisted so changes touch one place; the Tailwind @source scanner 20 + // picks the literal up via @source "./*.go" (which includes pages/). 21 + GuestbookContainer := "flex flex-col gap-5 bg-surface border border-border rounded-lg p-4 sm:p-[clamp(1.25rem,2.5vw,2rem)]" 17 22 --- 18 23 {{ wrap DocsLayout (dict "Title" .Title "Active" .Active) }} 19 24 <h1>Guestbook (Plain)</h1> 20 25 <p>A guestbook using traditional HTML forms and the Post/Redirect/Get pattern. No JavaScript, no libraries &mdash; just a <code>&lt;form&gt;</code> with <code>method="POST"</code>.</p> 21 26 22 - <div class="demo-section"> 23 - <div class="guestbook-container"> 24 - <div id="guestbook-list" class="guestbook-list" role="list" aria-label="Guestbook entries"> 27 + <div class="py-8"> 28 + <div class="{{ .GuestbookContainer }}"> 29 + <div id="guestbook-list" class="flex flex-col gap-3" role="list" aria-label="Guestbook entries"> 25 30 {{ range .Entries }} 26 31 {{ GuestbookEntry (dict "ID" .ID "Name" .Name "Message" .Message "Time" (.CreatedAt.Format "Jan 2, 3:04 PM")) }} 27 32 {{ end }} 28 33 {{ if not .Entries }} 29 - <p class="guestbook-empty">No entries yet. Be the first to sign the guestbook!</p> 34 + <p class="text-center text-text-subtle text-[0.9rem] py-6">No entries yet. Be the first to sign the guestbook!</p> 30 35 {{ end }} 31 36 </div> 32 37 33 - <form class="guestbook-form" method="POST" action="/guestbook"> 38 + <form class="flex flex-col gap-3" method="POST" action="/guestbook"> 34 39 {{ GuestbookForm (dict "Error" .Error "Success" .Success) }} 35 - <button type="submit" class="btn btn-primary guestbook-submit">Sign Guestbook</button> 40 + <button type="submit" class="btn btn-primary self-start">Sign Guestbook</button> 36 41 </form> 37 42 </div> 38 43 </div> 39 44 40 - <div class="demo-explanation"> 45 + <div class="mt-8"> 41 46 <h2>How It Works</h2> 42 47 <p>This is the simplest way to handle user input in Gastro. The <code>.gastro</code> page renders the form (GET), and a plain Go handler in <code>main.go</code> processes the submission (POST).</p> 43 48
+42 -40
examples/gastro/pages/index.gastro
··· 19 19 {{ Hero (dict "IntroHero" .IntroHero) }} 20 20 21 21 <main id="main-content"> 22 - <section class="features-section"> 23 - <div class="features-header"> 24 - <h2>Why Gastro?</h2> 25 - <p>Your programming language has strict types. Why doesn't that extend to templates and routing?</p> 22 + <section class="px-6 py-12 max-w-site mx-auto md:px-8 md:py-[clamp(3.5rem,6vw,5.5rem)]"> 23 + <div class="mb-8 max-w-[520px] md:mb-[clamp(2rem,4vw,3.5rem)]"> 24 + <h2 class="font-display font-bold tracking-[-0.02em] mb-2 leading-[1.2] text-[clamp(1.6rem,3vw,2.2rem)]">Why Gastro?</h2> 25 + <p class="text-text-muted text-[1.05rem] leading-[1.6]">Your programming language has strict types. Why doesn't that extend to templates and routing?</p> 26 26 </div> 27 - <div class="features-grid"> 28 - <div class="feature-card feature-card--featured"> 29 - <h3>File-Based Routing</h3> 30 - <p>Pages in your <code>pages/</code> directory automatically become routes. Dynamic parameters with <code>[slug]</code> syntax. No configuration needed &mdash; just create a file and it&rsquo;s live.</p> 27 + <div class="grid grid-cols-1 grid-rows-[auto] gap-5 md:grid-cols-2 md:grid-rows-[auto_auto_auto]"> 28 + {{- /* Feature cards. The first card spans rows 1-2 in the 29 + left column on desktop; on mobile it stacks naturally. */ -}} 30 + <div class="feature-card md:row-span-2 bg-surface flex flex-col justify-center p-[clamp(2rem,3vw,2.5rem)] [animation-delay:0.05s]"> 31 + <h3 class="font-display font-bold leading-[1.3] text-[clamp(1.2rem,2vw,1.4rem)] mb-[0.6rem]">File-Based Routing</h3> 32 + <p class="text-text-muted text-base leading-[1.6]">Pages in your <code>pages/</code> directory automatically become routes. Dynamic parameters with <code>[slug]</code> syntax. No configuration needed &mdash; just create a file and it&rsquo;s live.</p> 31 33 </div> 32 - <div class="feature-card"> 33 - <h3>Go Frontmatter</h3> 34 - <p>Write type-safe Go in the frontmatter, import libraries or services and make variables and functions available to your template.</p> 34 + <div class="feature-card [animation-delay:0.1s]"> 35 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">Go Frontmatter</h3> 36 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Write type-safe Go in the frontmatter, import libraries or services and make variables and functions available to your template.</p> 35 37 </div> 36 - <div class="feature-card"> 37 - <h3>HTML Body, using <code>html/template</code></h3> 38 - <p>Uses the standard Go templating engine. Syntax validation and autocomplete for variables and functions.</p> 38 + <div class="feature-card [animation-delay:0.15s]"> 39 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">HTML Body, using <code>html/template</code></h3> 40 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Uses the standard Go templating engine. Syntax validation and autocomplete for variables and functions.</p> 39 41 </div> 40 42 41 - <div class="feature-card"> 42 - <h3>Editors &amp; Coding Agents</h3> 43 - <p>Full language server (LSP) with completions, diagnostics, and go-to-definition. For both frontmatter Go in the frontmatter, and HTML templates in the body.</p> 44 - <p>Extensions for VS Code, Zed and Nevim. More can be created as interest grows.</p> 43 + <div class="feature-card [animation-delay:0.2s]"> 44 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">Editors &amp; Coding Agents</h3> 45 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Full language server (LSP) with completions, diagnostics, and go-to-definition. For both frontmatter Go in the frontmatter, and HTML templates in the body.</p> 46 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Extensions for VS Code, Zed and Nevim. More can be created as interest grows.</p> 45 47 </div> 46 - <div class="feature-card"> 47 - <h3>Single Binary Deploy</h3> 48 - <p>Templates and static assets are embedded at build time. Zero runtime dependencies. Ship one file anywhere.</p> 48 + <div class="feature-card [animation-delay:0.25s]"> 49 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">Single Binary Deploy</h3> 50 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Templates and static assets are embedded at build time. Zero runtime dependencies. Ship one file anywhere.</p> 49 51 </div> 50 - <div class="feature-card"> 51 - <h3>Idiomatic Go</h3> 52 - <p>Standard Go syntax you already know. <code>html/template</code>, <code>net/http</code>, <code>go:embed</code> &mdash; no new language to learn, full type-safety throughout.</p> 53 - <p>The project is still a Go project and you can build it as you see fit. We just take away the pain of managing page handlers and templates.</p> 52 + <div class="feature-card [animation-delay:0.3s]"> 53 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">Idiomatic Go</h3> 54 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Standard Go syntax you already know. <code>html/template</code>, <code>net/http</code>, <code>go:embed</code> &mdash; no new language to learn, full type-safety throughout.</p> 55 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">The project is still a Go project and you can build it as you see fit. We just take away the pain of managing page handlers and templates.</p> 54 56 </div> 55 - <div class="feature-card"> 56 - <h3>Hypermedia Ready</h3> 57 - <p>Use with <a href="examples/guestbook-datastar">Datastar</a> or <a href="examples/guestbook-htmx">htmx</a> for awesome user-experience and dynamic functionality that was once reserved for single-page-applications.</p> 57 + <div class="feature-card [animation-delay:0.35s]"> 58 + <h3 class="font-display font-bold leading-[1.3] text-[1.1rem] mb-[0.4rem]">Hypermedia Ready</h3> 59 + <p class="text-text-muted text-[0.93rem] leading-[1.6]">Use with <a href="examples/guestbook-datastar">Datastar</a> or <a href="examples/guestbook-htmx">htmx</a> for awesome user-experience and dynamic functionality that was once reserved for single-page-applications.</p> 58 60 </div> 59 61 </div> 60 62 </section> 61 - <section class="quickstart-section"> 62 - <div class="quickstart-container"> 63 - <div class="quickstart-header"> 64 - <h2>Quick Start</h2> 65 - <p>Get a Gastro project running in under a minute.</p> 63 + <section class="bg-surface border-y border-border-light px-6 py-12 md:px-8 md:py-[clamp(3.5rem,6vw,5.5rem)]"> 64 + <div class="max-w-site mx-auto"> 65 + <div class="mb-8 max-w-[480px] md:mb-[clamp(2rem,3vw,3rem)]"> 66 + <h2 class="font-display font-bold tracking-[-0.02em] mb-2 leading-[1.2] text-[clamp(1.6rem,3vw,2.2rem)]">Quick Start</h2> 67 + <p class="text-text-muted text-[1.05rem]">Get a Gastro project running in under a minute.</p> 66 68 </div> 67 - <div class="quickstart-steps"> 68 - <div class="quickstart-step"> 69 - <h3>Write a component</h3> 69 + <div class="grid grid-cols-1 gap-6 md:grid-cols-2 [&>*:first-child]:col-span-full"> 70 + <div class="bg-bg border border-border rounded-lg p-6"> 71 + <h3 class="font-mono text-[0.78rem] font-medium text-accent tracking-[0.02em] mb-4">Write a component</h3> 70 72 <div class="code-block"> 71 73 {{ .IntroComponent }} 72 74 </div> 73 75 </div> 74 - <div class="quickstart-step"> 75 - <h3>Develop</h3> 76 + <div class="bg-bg border border-border rounded-lg p-6"> 77 + <h3 class="font-mono text-[0.78rem] font-medium text-accent tracking-[0.02em] mb-4">Develop</h3> 76 78 <div class="code-block"> 77 79 <pre><code class="language-bash"># Start the dev server 78 80 gastro dev ··· 83 85 </code></pre> 84 86 </div> 85 87 </div> 86 - <div class="quickstart-step"> 87 - <h3>Deploy</h3> 88 + <div class="bg-bg border border-border rounded-lg p-6"> 89 + <h3 class="font-mono text-[0.78rem] font-medium text-accent tracking-[0.02em] mb-4">Deploy</h3> 88 90 <div class="code-block"> 89 91 <pre><code class="language-bash"># Build a single binary 90 92 gastro build
+2 -1359
examples/gastro/static/styles.css
··· 1 - /* Gastro — a file-based component framework for Go */ 2 - 3 - *, 4 - *::before, 5 - *::after { 6 - margin: 0; 7 - padding: 0; 8 - box-sizing: border-box; 9 - } 10 - 11 - :root { 12 - --color-bg: #fffbf7; 13 - --color-text: #2c2418; 14 - --color-text-muted: #7a6e5d; 15 - --color-text-subtle: #a89f90; 16 - --color-accent: #e8722a; 17 - --color-accent-hover: #d15f1a; 18 - --color-accent-light: #fef3eb; 19 - --color-accent-warm: #f5a623; 20 - --color-border: #e8e0d4; 21 - --color-border-light: #f2ece4; 22 - --color-surface: #f7f2ec; 23 - --color-code-bg: #2a2520; 24 - --color-code-text: #e0d6c8; 25 - --color-header-bg: #2c2418; 26 - --color-header-text: #f7f2ec; 27 - --color-hero-bg: #2c2418; 28 - --font-display: "Source Serif 4", "Georgia", serif; 29 - --font-sans: "DM Sans", system-ui, -apple-system, sans-serif; 30 - --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace; 31 - --max-width: 1120px; 32 - --radius: 6px; 33 - --radius-lg: 10px; 34 - } 35 - 36 - html { 37 - font-size: 16px; 38 - scroll-behavior: smooth; 39 - } 40 - 41 - body { 42 - font-family: var(--font-sans); 43 - line-height: 1.7; 44 - color: var(--color-text); 45 - background: var(--color-bg); 46 - -webkit-font-smoothing: antialiased; 47 - -moz-osx-font-smoothing: grayscale; 48 - } 49 - 50 - a { 51 - color: var(--color-accent); 52 - text-decoration: none; 53 - transition: color 0.15s ease; 54 - } 55 - 56 - a:hover { 57 - color: var(--color-accent-hover); 58 - } 59 - 60 - code { 61 - font-family: var(--font-mono); 62 - font-size: 0.875em; 63 - } 64 - 65 - p code, 66 - li code, 67 - td code, 68 - h1 code, 69 - h2 code, 70 - h3 code { 71 - background: var(--color-surface); 72 - border: 1px solid var(--color-border); 73 - padding: 0.1em 0.35em; 74 - border-radius: 4px; 75 - font-size: 0.84em; 76 - color: var(--color-accent-hover); 77 - } 78 - 79 - /* Skip link for keyboard/screen-reader users */ 80 - .skip-link { 81 - position: absolute; 82 - top: -100%; 83 - left: 1rem; 84 - padding: 0.5rem 1rem; 85 - background: var(--color-accent); 86 - color: white; 87 - border-radius: var(--radius); 88 - font-weight: 600; 89 - font-size: 0.9rem; 90 - z-index: 200; 91 - transition: top 0.15s ease; 92 - } 93 - 94 - .skip-link:focus { 95 - top: 0.5rem; 96 - color: white; 97 - } 98 - 99 - .sr-only { 100 - position: absolute; 101 - width: 1px; 102 - height: 1px; 103 - padding: 0; 104 - margin: -1px; 105 - overflow: hidden; 106 - clip: rect(0, 0, 0, 0); 107 - white-space: nowrap; 108 - border: 0; 109 - } 110 - 111 - /* ------------------------------------------------------- 112 - Site Header 113 - ------------------------------------------------------- */ 114 - 115 - .site-header { 116 - background: var(--color-header-bg); 117 - position: sticky; 118 - top: 0; 119 - z-index: 100; 120 - } 121 - 122 - .nav-container { 123 - max-width: var(--max-width); 124 - margin: 0 auto; 125 - padding: 0 2rem; 126 - height: 56px; 127 - display: flex; 128 - align-items: center; 129 - justify-content: space-between; 130 - } 131 - 132 - .nav-logo { 133 - display: flex; 134 - align-items: center; 135 - color: var(--color-header-text); 136 - } 137 - 138 - .nav-logo img { 139 - height: 26px; 140 - } 141 - 142 - .nav-links { 143 - display: flex; 144 - gap: 1.75rem; 145 - align-items: center; 146 - } 147 - 148 - .nav-links a { 149 - color: var(--color-text-subtle); 150 - font-size: 0.88rem; 151 - font-weight: 500; 152 - letter-spacing: 0.01em; 153 - transition: color 0.15s ease; 154 - } 155 - 156 - .nav-links a:hover { 157 - color: var(--color-header-text); 158 - } 159 - 160 - /* ------------------------------------------------------- 161 - Hero Section 162 - ------------------------------------------------------- */ 163 - 164 - .hero-wrapper { 165 - background: var(--color-hero-bg); 166 - } 167 - 168 - .hero { 169 - display: grid; 170 - grid-template-columns: 1.1fr 0.9fr; 171 - gap: clamp(2rem, 4vw, 5rem); 172 - max-width: var(--max-width); 173 - margin: 0 auto; 174 - padding: clamp(3rem, 6vw, 6rem) 2rem clamp(3.5rem, 7vw, 7rem); 175 - align-items: center; 176 - } 177 - 178 - .hero-content { 179 - display: flex; 180 - flex-direction: column; 181 - } 182 - 183 - .hero-eyebrow { 184 - font-family: var(--font-mono); 185 - font-size: 0.82rem; 186 - color: var(--color-accent-warm); 187 - letter-spacing: 0.04em; 188 - margin-bottom: 1rem; 189 - font-weight: 500; 190 - } 191 - 192 - .hero-title { 193 - font-family: var(--font-display); 194 - font-size: clamp(2rem, 4vw, 3rem); 195 - font-weight: 700; 196 - line-height: 1.15; 197 - color: var(--color-header-text); 198 - letter-spacing: -0.02em; 199 - margin-bottom: 1.25rem; 200 - } 201 - 202 - .hero-accent { 203 - color: var(--color-accent); 204 - } 205 - 206 - .hero-description { 207 - font-size: clamp(1rem, 1.5vw, 1.1rem); 208 - color: var(--color-text-subtle); 209 - line-height: 1.7; 210 - margin-bottom: 2rem; 211 - max-width: 480px; 212 - } 213 - 214 - .hero-actions { 215 - display: flex; 216 - gap: 0.75rem; 217 - flex-wrap: wrap; 218 - } 219 - 220 - /* ------------------------------------------------------- 221 - Buttons 222 - ------------------------------------------------------- */ 223 - 224 - .btn { 225 - display: inline-flex; 226 - align-items: center; 227 - justify-content: center; 228 - padding: 0.65rem 1.4rem; 229 - border-radius: var(--radius); 230 - font-size: 0.92rem; 231 - font-weight: 600; 232 - text-decoration: none; 233 - transition: all 0.2s ease; 234 - border: 1.5px solid transparent; 235 - cursor: pointer; 236 - font-family: var(--font-sans); 237 - } 238 - 239 - .btn-primary { 240 - background: var(--color-accent); 241 - color: white; 242 - border-color: var(--color-accent); 243 - } 244 - 245 - .btn-primary:hover { 246 - background: var(--color-accent-hover); 247 - border-color: var(--color-accent-hover); 248 - color: white; 249 - transform: translateY(-1px); 250 - } 251 - 252 - .btn-secondary { 253 - background: transparent; 254 - color: var(--color-text-subtle); 255 - border-color: rgba(255, 255, 255, 0.15); 256 - } 257 - 258 - .btn-secondary:hover { 259 - border-color: rgba(255, 255, 255, 0.35); 260 - color: var(--color-header-text); 261 - } 262 - 263 - /* ------------------------------------------------------- 264 - Code Window (Hero) 265 - ------------------------------------------------------- */ 266 - 267 - .hero-code { 268 - min-width: 0; 269 - } 270 - 271 - .code-window { 272 - background: var(--color-code-bg); 273 - border-radius: var(--radius-lg); 274 - overflow: hidden; 275 - color: var(--color-accent-light); 276 - box-shadow: 0 20px 40px -12px rgba(44, 36, 24, 0.45), 277 - 0 0 0 1px rgba(255, 255, 255, 0.04); 278 - } 279 - 280 - .code-window-bar { 281 - display: flex; 282 - align-items: center; 283 - padding: 0.7rem 1rem; 284 - background: rgba(0, 0, 0, 0.25); 285 - gap: 0.5rem; 286 - } 287 - 288 - .code-dot { 289 - width: 10px; 290 - height: 10px; 291 - border-radius: 50%; 292 - } 293 - 294 - .code-dot.red { 295 - background: #e85c4a; 296 - } 297 - 298 - .code-dot.yellow { 299 - background: #e8a73e; 300 - } 301 - 302 - .code-dot.green { 303 - background: #5eb86c; 304 - } 305 - 306 - .code-window-title { 307 - margin-left: 0.5rem; 308 - font-size: 0.78rem; 309 - color: var(--color-text-subtle); 310 - font-family: var(--font-mono); 311 - } 312 - 313 - .code-window pre, 314 - .code-window pre[class*="language-"] { 315 - margin: 0; 316 - padding: 1.25rem; 317 - overflow-x: auto; 318 - background: var(--color-code-bg); 319 - } 320 - 321 - .code-window pre code { 322 - font-size: 0.84rem; 323 - line-height: 1.65; 324 - } 325 - 326 - .code-window code[class*="language-"] { 327 - background: transparent; 328 - } 329 - 330 - /* ------------------------------------------------------- 331 - Features Section 332 - ------------------------------------------------------- */ 333 - 334 - .features-section { 335 - padding: clamp(3.5rem, 6vw, 5.5rem) 2rem; 336 - max-width: var(--max-width); 337 - margin: 0 auto; 338 - } 339 - 340 - .features-header { 341 - margin-bottom: clamp(2rem, 4vw, 3.5rem); 342 - max-width: 520px; 343 - } 344 - 345 - .features-header h2 { 346 - font-family: var(--font-display); 347 - font-size: clamp(1.6rem, 3vw, 2.2rem); 348 - font-weight: 700; 349 - letter-spacing: -0.02em; 350 - margin-bottom: 0.5rem; 351 - line-height: 1.2; 352 - } 353 - 354 - .features-header p { 355 - color: var(--color-text-muted); 356 - font-size: 1.05rem; 357 - line-height: 1.6; 358 - } 359 - 360 - .features-grid { 361 - display: grid; 362 - grid-template-columns: 1fr 1fr; 363 - grid-template-rows: auto auto auto; 364 - gap: 1.25rem; 365 - } 366 - 367 - /* Featured card spans the left column fully */ 368 - .feature-card { 369 - background: var(--color-bg); 370 - border: 1px solid var(--color-border); 371 - border-radius: var(--radius-lg); 372 - padding: clamp(1.5rem, 2.5vw, 2rem); 373 - transition: border-color 0.2s ease, box-shadow 0.2s ease; 374 - } 375 - 376 - .feature-card:hover { 377 - border-color: var(--color-accent); 378 - box-shadow: 0 2px 12px rgba(232, 114, 42, 0.06); 379 - } 380 - 381 - .feature-card--featured { 382 - grid-row: 1 / 3; 383 - background: var(--color-surface); 384 - display: flex; 385 - flex-direction: column; 386 - justify-content: center; 387 - padding: clamp(2rem, 3vw, 2.5rem); 388 - } 389 - 390 - .feature-card h3 { 391 - font-family: var(--font-display); 392 - font-size: 1.1rem; 393 - font-weight: 700; 394 - margin-bottom: 0.4rem; 395 - line-height: 1.3; 396 - } 397 - 398 - .feature-card--featured h3 { 399 - font-size: clamp(1.2rem, 2vw, 1.4rem); 400 - margin-bottom: 0.6rem; 401 - } 402 - 403 - .feature-card p { 404 - color: var(--color-text-muted); 405 - font-size: 0.93rem; 406 - line-height: 1.6; 407 - } 408 - 409 - .feature-card--featured p { 410 - font-size: 1rem; 411 - } 412 - 413 - .feature-tag { 414 - display: inline-block; 415 - font-family: var(--font-mono); 416 - font-size: 0.72rem; 417 - font-weight: 500; 418 - color: var(--color-accent); 419 - background: var(--color-accent-light); 420 - padding: 0.15em 0.5em; 421 - border-radius: 3px; 422 - margin-bottom: 0.75rem; 423 - letter-spacing: 0.02em; 424 - } 425 - 426 - /* ------------------------------------------------------- 427 - Quick Start Section 428 - ------------------------------------------------------- */ 429 - 430 - .quickstart-section { 431 - background: var(--color-surface); 432 - border-top: 1px solid var(--color-border-light); 433 - border-bottom: 1px solid var(--color-border-light); 434 - padding: clamp(3.5rem, 6vw, 5.5rem) 2rem; 435 - } 436 - 437 - .quickstart-container { 438 - max-width: var(--max-width); 439 - margin: 0 auto; 440 - } 441 - 442 - .quickstart-header { 443 - margin-bottom: clamp(2rem, 3vw, 3rem); 444 - max-width: 480px; 445 - } 446 - 447 - .quickstart-header h2 { 448 - font-family: var(--font-display); 449 - font-size: clamp(1.6rem, 3vw, 2.2rem); 450 - font-weight: 700; 451 - letter-spacing: -0.02em; 452 - margin-bottom: 0.5rem; 453 - line-height: 1.2; 454 - } 455 - 456 - .quickstart-header p { 457 - color: var(--color-text-muted); 458 - font-size: 1.05rem; 459 - } 460 - 461 - .quickstart-steps { 462 - display: grid; 463 - grid-template-columns: 1fr 1fr; 464 - gap: 1.5rem; 465 - } 466 - 467 - .quickstart-step:first-child { 468 - grid-column: 1 / -1; 469 - } 470 - 471 - .quickstart-step { 472 - background: var(--color-bg); 473 - border: 1px solid var(--color-border); 474 - border-radius: var(--radius-lg); 475 - padding: 1.5rem; 476 - } 477 - 478 - .quickstart-step h3 { 479 - font-family: var(--font-mono); 480 - font-size: 0.78rem; 481 - font-weight: 500; 482 - color: var(--color-accent); 483 - letter-spacing: 0.02em; 484 - margin-bottom: 1rem; 485 - } 486 - 487 - /* ------------------------------------------------------- 488 - Docs Layout 489 - ------------------------------------------------------- */ 490 - 491 - .docs-container { 492 - max-width: var(--max-width); 493 - margin: 0 auto; 494 - padding: 2rem; 495 - display: grid; 496 - grid-template-columns: 210px 1fr; 497 - gap: 3rem; 498 - min-height: calc(100vh - 56px - 100px); 499 - } 500 - 501 - .docs-sidebar { 502 - position: sticky; 503 - top: 72px; 504 - align-self: start; 505 - padding-top: 0.75rem; 506 - } 507 - 508 - .docs-sidebar-toggle { 509 - display: none; 510 - } 511 - 512 - .docs-sidebar h3 { 513 - font-family: var(--font-mono); 514 - font-size: 0.7rem; 515 - font-weight: 600; 516 - text-transform: uppercase; 517 - letter-spacing: 0.08em; 518 - color: var(--color-text-subtle); 519 - margin-bottom: 0.6rem; 520 - margin-top: 1.5rem; 521 - } 522 - 523 - .docs-sidebar-nav h3:first-child { 524 - margin-top: 0; 525 - } 526 - 527 - .docs-sidebar ul { 528 - list-style: none; 529 - padding: 0; 530 - margin: 0; 531 - } 532 - 533 - .docs-sidebar li { 534 - margin-bottom: 0.15rem; 535 - } 536 - 537 - .docs-sidebar a { 538 - display: block; 539 - padding: 0.3rem 0.7rem; 540 - border-radius: 5px; 541 - color: var(--color-text-muted); 542 - font-size: 0.88rem; 543 - font-weight: 500; 544 - transition: all 0.15s ease; 545 - } 546 - 547 - .docs-sidebar a:hover { 548 - color: var(--color-text); 549 - background: var(--color-surface); 550 - } 551 - 552 - .docs-sidebar a.active { 553 - color: var(--color-accent); 554 - background: var(--color-accent-light); 555 - font-weight: 600; 556 - } 557 - 558 - /* ------------------------------------------------------- 559 - Docs Content 560 - ------------------------------------------------------- */ 561 - 562 - .docs-content { 563 - padding-top: 0.75rem; 564 - padding-bottom: 4rem; 565 - max-width: 720px; 566 - } 567 - 568 - .docs-content h1 { 569 - font-family: var(--font-display); 570 - font-size: clamp(1.75rem, 3vw, 2.1rem); 571 - font-weight: 700; 572 - margin-bottom: 0.4rem; 573 - letter-spacing: -0.02em; 574 - line-height: 1.2; 575 - } 576 - 577 - .docs-content h1+p { 578 - font-size: 1.05rem; 579 - color: var(--color-text-muted); 580 - margin-bottom: 2rem; 581 - line-height: 1.7; 582 - } 583 - 584 - .docs-content h2 { 585 - font-family: var(--font-display); 586 - font-size: clamp(1.2rem, 2vw, 1.35rem); 587 - font-weight: 700; 588 - margin-top: 2.75rem; 589 - margin-bottom: 0.75rem; 590 - padding-top: 1.5rem; 591 - border-top: 1px solid var(--color-border-light); 592 - letter-spacing: -0.01em; 593 - line-height: 1.3; 594 - } 595 - 596 - .docs-content h2:first-of-type { 597 - margin-top: 1.75rem; 598 - border-top: none; 599 - padding-top: 0; 600 - } 601 - 602 - .docs-content h3 { 603 - font-family: var(--font-display); 604 - font-size: 1.05rem; 605 - font-weight: 700; 606 - margin-top: 1.75rem; 607 - margin-bottom: 0.6rem; 608 - line-height: 1.3; 609 - } 610 - 611 - .docs-content p { 612 - margin-bottom: 1rem; 613 - color: var(--color-text); 614 - } 615 - 616 - .docs-content ul, 617 - .docs-content ol { 618 - margin-bottom: 1.25rem; 619 - padding-left: 1.5rem; 620 - } 621 - 622 - .docs-content li { 623 - margin-bottom: 0.35rem; 624 - color: var(--color-text); 625 - } 626 - 627 - .docs-content strong { 628 - font-weight: 600; 629 - } 630 - 631 - .docs-content blockquote { 632 - border-left: 3px solid var(--color-accent); 633 - margin: 1.5rem 0; 634 - padding: 0.7rem 1.2rem; 635 - background: var(--color-accent-light); 636 - border-radius: 0 var(--radius) var(--radius) 0; 637 - } 638 - 639 - .docs-content blockquote p { 640 - margin: 0; 641 - color: var(--color-text); 642 - } 643 - 644 - /* ------------------------------------------------------- 645 - Code Blocks (Docs) 646 - ------------------------------------------------------- */ 647 - 648 - .code-block { 649 - margin: 1.25rem 0; 650 - border-radius: var(--radius); 651 - overflow: hidden; 652 - border: 1px solid rgba(42, 37, 32, 0.08); 653 - } 654 - 655 - .code-block pre, 656 - .code-block pre[class*="language-"] { 657 - margin: 0; 658 - padding: 1.2rem; 659 - overflow-x: auto; 660 - background: var(--color-code-bg); 661 - } 662 - 663 - .code-block pre code { 664 - font-size: 0.84rem; 665 - line-height: 1.65; 666 - color: var(--color-code-text); 667 - } 668 - 669 - .code-block code[class*="language-"] { 670 - background: transparent; 671 - } 672 - 673 - /* ------------------------------------------------------- 674 - Mermaid diagrams (rendered client-side, see layout.gastro) 675 - ------------------------------------------------------- 676 - Hide the raw mermaid source until mermaid.js has swapped 677 - it for an SVG (it sets data-processed="true" when done). 678 - visibility:hidden preserves layout space so we don't 679 - reflow on render. The SVG itself inherits the cream page 680 - background via theme=base + transparent canvas. 681 - ------------------------------------------------------- */ 682 - 683 - .docs-content pre.mermaid { 684 - margin: 1.5rem 0; 685 - padding: 0; 686 - background: transparent; 687 - border: 0; 688 - text-align: center; 689 - overflow-x: auto; 690 - } 691 - 692 - .docs-content pre.mermaid:not([data-processed="true"]) { 693 - visibility: hidden; 694 - min-height: 6rem; 695 - } 696 - 697 - .docs-content pre.mermaid svg { 698 - max-width: 100%; 699 - height: auto; 700 - } 701 - 702 - /* ------------------------------------------------------- 703 - Markdown-rendered code blocks (chroma, classed output) 704 - ------------------------------------------------------- 705 - Site-specific muted palette. Kept in styles.css (not 706 - static/chroma.css) so the website theme is self-contained 707 - and independent of the `gastro new` scaffold's github theme. 708 - ------------------------------------------------------- */ 709 - 710 - .docs-content pre.chroma { 711 - margin: 1.25rem 0; 712 - padding: 1.2rem; 713 - border-radius: var(--radius); 714 - border: 1px solid rgba(42, 37, 32, 0.08); 715 - overflow-x: auto; 716 - background: var(--color-code-bg); 717 - color: var(--color-code-text); 718 - font-size: 0.84rem; 719 - line-height: 1.65; 720 - } 721 - 722 - .docs-content pre.chroma code { 723 - background: transparent; 724 - color: inherit; 725 - padding: 0; 726 - font-size: inherit; 727 - } 728 - 729 - /* Chroma token colors — muted palette that works on dark code bg */ 730 - .chroma .k, 731 - .chroma .kd, 732 - .chroma .kn, 733 - .chroma .kp, 734 - .chroma .kr, 735 - .chroma .kt { 736 - color: #c98dff; 737 - } 738 - 739 - /* keyword */ 740 - .chroma .s, 741 - .chroma .sa, 742 - .chroma .sb, 743 - .chroma .sc, 744 - .chroma .s1, 745 - .chroma .s2 { 746 - color: #a5d96b; 747 - } 748 - 749 - /* string */ 750 - .chroma .c, 751 - .chroma .c1, 752 - .chroma .cm, 753 - .chroma .cs { 754 - color: #8a8170; 755 - font-style: italic; 756 - } 757 - 758 - /* comment */ 759 - .chroma .cp { 760 - color: #8a8170; 761 - font-style: italic; 762 - } 763 - 764 - /* preproc — used for gastro frontmatter --- and {{ ... }} */ 765 - .chroma .n, 766 - .chroma .nx, 767 - .chroma .nv { 768 - color: var(--color-code-text); 769 - } 770 - 771 - .chroma .nf, 772 - .chroma .nc { 773 - color: #7ec4ff; 774 - } 775 - 776 - /* function / component names */ 777 - .chroma .nb { 778 - color: #7ec4ff; 779 - } 780 - 781 - /* builtin */ 782 - .chroma .nt { 783 - color: #ff9e75; 784 - } 785 - 786 - /* HTML tag name */ 787 - .chroma .na { 788 - color: #e8a860; 789 - } 790 - 791 - /* HTML / component attribute name */ 792 - .chroma .m, 793 - .chroma .mi, 794 - .chroma .mf { 795 - color: #e8a860; 796 - } 797 - 798 - /* numbers */ 799 - .chroma .o, 800 - .chroma .p { 801 - color: #d6c9b4; 802 - } 803 - 804 - /* operators / punctuation */ 805 - .chroma .err { 806 - color: #ff8b6e; 807 - } 808 - 809 - /* ------------------------------------------------------- 810 - Tables (Docs) 811 - ------------------------------------------------------- */ 812 - 813 - .docs-content table { 814 - width: 100%; 815 - border-collapse: collapse; 816 - margin: 1.25rem 0; 817 - font-size: 0.9rem; 818 - } 819 - 820 - .docs-content th { 821 - text-align: left; 822 - padding: 0.6rem 0.85rem; 823 - background: var(--color-surface); 824 - border: 1px solid var(--color-border); 825 - font-weight: 600; 826 - font-size: 0.84rem; 827 - } 828 - 829 - .docs-content td { 830 - padding: 0.55rem 0.85rem; 831 - border: 1px solid var(--color-border); 832 - vertical-align: top; 833 - } 834 - 835 - /* ------------------------------------------------------- 836 - Comparison Table 837 - ------------------------------------------------------- */ 838 - 839 - .comparison-table-wrapper { 840 - overflow-x: auto; 841 - margin: 1.25rem 0; 842 - border: 1px solid var(--color-border); 843 - border-radius: var(--radius); 844 - -webkit-overflow-scrolling: touch; 845 - } 846 - 847 - .comparison-table { 848 - width: 100%; 849 - border-collapse: collapse; 850 - font-size: 0.88rem; 851 - min-width: 720px; 852 - } 853 - 854 - .comparison-table thead th { 855 - position: sticky; 856 - top: 0; 857 - background: var(--color-surface); 858 - font-weight: 700; 859 - font-size: 0.84rem; 860 - padding: 0.65rem 0.85rem; 861 - border-bottom: 2px solid var(--color-border); 862 - text-align: left; 863 - white-space: nowrap; 864 - } 865 - 866 - .comparison-table thead th:first-child { 867 - min-width: 120px; 868 - } 869 - 870 - .comparison-table tbody td { 871 - padding: 0.6rem 0.85rem; 872 - border-bottom: 1px solid var(--color-border-light); 873 - vertical-align: top; 874 - line-height: 1.5; 875 - } 876 - 877 - .comparison-table tbody td:first-child { 878 - white-space: nowrap; 879 - background: var(--color-surface); 880 - font-size: 0.84rem; 881 - } 882 - 883 - .comparison-table tbody tr:last-child td { 884 - border-bottom: none; 885 - } 886 - 887 - /* ------------------------------------------------------- 888 - Guestbook & Demo 889 - ------------------------------------------------------- */ 890 - 891 - .demo-section { 892 - padding: 2rem 0; 893 - } 894 - 895 - .demo-explanation { 896 - margin-top: 2rem; 897 - } 898 - 899 - .guestbook-container { 900 - background: var(--color-surface); 901 - border: 1px solid var(--color-border); 902 - border-radius: var(--radius-lg); 903 - padding: clamp(1.25rem, 2.5vw, 2rem); 904 - display: flex; 905 - flex-direction: column; 906 - gap: 1.25rem; 907 - } 908 - 909 - .guestbook-search-wrapper { 910 - position: relative; 911 - } 912 - 913 - .guestbook-input { 914 - width: 100%; 915 - padding: 0.6rem 0.85rem; 916 - border: 1px solid var(--color-border); 917 - border-radius: var(--radius); 918 - font-family: var(--font-sans); 919 - font-size: 0.9rem; 920 - color: var(--color-text); 921 - background: var(--color-bg); 922 - transition: border-color 0.15s ease; 923 - } 924 - 925 - .guestbook-input:focus { 926 - outline: none; 927 - border-color: var(--color-accent); 928 - box-shadow: 0 0 0 3px rgba(232, 114, 42, 0.1); 929 - } 930 - 931 - .guestbook-input::placeholder { 932 - color: var(--color-text-subtle); 933 - } 934 - 935 - .guestbook-textarea { 936 - min-height: 80px; 937 - resize: vertical; 938 - line-height: 1.5; 939 - } 940 - 941 - .guestbook-search { 942 - padding-right: 2.5rem; 943 - } 944 - 945 - .guestbook-list { 946 - display: flex; 947 - flex-direction: column; 948 - gap: 0.75rem; 949 - } 950 - 951 - .guestbook-entry { 952 - background: var(--color-bg); 953 - border: 1px solid var(--color-border); 954 - border-radius: var(--radius); 955 - padding: 0.85rem 1rem; 956 - transition: border-color 0.15s ease; 957 - position: relative; 958 - } 959 - 960 - .guestbook-entry:hover { 961 - border-color: var(--color-accent); 962 - } 963 - 964 - .guestbook-entry--editing { 965 - border-color: var(--color-accent); 966 - box-shadow: 0 0 0 3px rgba(232, 114, 42, 0.08); 967 - } 968 - 969 - .guestbook-entry-header { 970 - display: flex; 971 - justify-content: space-between; 972 - align-items: baseline; 973 - margin-bottom: 0.35rem; 974 - gap: 0.5rem; 975 - } 976 - 977 - .guestbook-entry-name { 978 - font-size: 0.9rem; 979 - font-weight: 600; 980 - color: var(--color-text); 981 - } 982 - 983 - .guestbook-entry-time { 984 - font-family: var(--font-mono); 985 - font-size: 0.72rem; 986 - color: var(--color-text-subtle); 987 - white-space: nowrap; 988 - } 989 - 990 - .guestbook-entry-message { 991 - font-size: 0.9rem; 992 - color: var(--color-text-muted); 993 - line-height: 1.5; 994 - margin: 0; 995 - } 996 - 997 - .guestbook-edit-btn { 998 - position: absolute; 999 - top: 0.6rem; 1000 - right: 0.6rem; 1001 - background: none; 1002 - border: 1px solid var(--color-border); 1003 - border-radius: 4px; 1004 - padding: 0.2rem 0.5rem; 1005 - font-family: var(--font-mono); 1006 - font-size: 0.7rem; 1007 - color: var(--color-text-subtle); 1008 - cursor: pointer; 1009 - transition: all 0.15s ease; 1010 - opacity: 0; 1011 - } 1012 - 1013 - .guestbook-entry:hover .guestbook-edit-btn { 1014 - opacity: 1; 1015 - } 1016 - 1017 - .guestbook-edit-btn:hover { 1018 - color: var(--color-accent); 1019 - border-color: var(--color-accent); 1020 - } 1021 - 1022 - .guestbook-edit-actions { 1023 - display: flex; 1024 - gap: 0.5rem; 1025 - margin-top: 0.6rem; 1026 - } 1027 - 1028 - .guestbook-empty { 1029 - text-align: center; 1030 - color: var(--color-text-subtle); 1031 - font-size: 0.9rem; 1032 - padding: 1.5rem 0; 1033 - } 1034 - 1035 - .guestbook-form { 1036 - display: flex; 1037 - flex-direction: column; 1038 - gap: 0.75rem; 1039 - } 1040 - 1041 - .guestbook-form-fields { 1042 - display: flex; 1043 - flex-direction: column; 1044 - gap: 0.6rem; 1045 - } 1046 - 1047 - .guestbook-submit-wrapper { 1048 - display: flex; 1049 - align-items: center; 1050 - gap: 0.75rem; 1051 - } 1052 - 1053 - .guestbook-submit { 1054 - align-self: flex-start; 1055 - } 1056 - 1057 - .guestbook-alert { 1058 - padding: 0.6rem 0.85rem; 1059 - border-radius: var(--radius); 1060 - font-size: 0.88rem; 1061 - font-weight: 500; 1062 - } 1063 - 1064 - .guestbook-alert--success { 1065 - background: #e8f5e9; 1066 - color: #2e7d32; 1067 - border: 1px solid #c8e6c9; 1068 - } 1069 - 1070 - .guestbook-alert--error { 1071 - background: #fef3eb; 1072 - color: #c62828; 1073 - border: 1px solid #ffe0cc; 1074 - } 1075 - 1076 - .btn-sm { 1077 - padding: 0.35rem 0.85rem; 1078 - font-size: 0.82rem; 1079 - } 1080 - 1081 - .loading-indicator { 1082 - position: absolute; 1083 - top: 50%; 1084 - right: 0.75rem; 1085 - transform: translateY(-50%); 1086 - } 1087 - 1088 - .spinner { 1089 - width: 16px; 1090 - height: 16px; 1091 - border: 2px solid var(--color-border); 1092 - border-top-color: var(--color-accent); 1093 - border-radius: 50%; 1094 - animation: spin 0.6s linear infinite; 1095 - } 1096 - 1097 - @keyframes spin { 1098 - to { 1099 - transform: rotate(360deg); 1100 - } 1101 - } 1102 - 1103 - /* HTMX loading indicator pattern */ 1104 - .htmx-indicator { 1105 - display: none; 1106 - } 1107 - 1108 - .htmx-request .htmx-indicator, 1109 - .htmx-request.htmx-indicator { 1110 - display: block; 1111 - } 1112 - 1113 - /* ------------------------------------------------------- 1114 - Next/Prev Navigation (Docs) 1115 - ------------------------------------------------------- */ 1116 - 1117 - .docs-nav { 1118 - display: flex; 1119 - justify-content: space-between; 1120 - margin-top: 3.5rem; 1121 - padding-top: 1.75rem; 1122 - border-top: 1px solid var(--color-border); 1123 - gap: 1rem; 1124 - } 1125 - 1126 - .docs-nav a { 1127 - display: block; 1128 - padding: 0.85rem 1.1rem; 1129 - border: 1px solid var(--color-border); 1130 - border-radius: var(--radius); 1131 - transition: border-color 0.2s ease; 1132 - min-width: 170px; 1133 - } 1134 - 1135 - .docs-nav a:hover { 1136 - border-color: var(--color-accent); 1137 - } 1138 - 1139 - .docs-nav-label { 1140 - font-family: var(--font-mono); 1141 - font-size: 0.7rem; 1142 - color: var(--color-text-subtle); 1143 - font-weight: 500; 1144 - letter-spacing: 0.04em; 1145 - margin-bottom: 0.2rem; 1146 - } 1147 - 1148 - .docs-nav-title { 1149 - font-weight: 600; 1150 - color: var(--color-text); 1151 - font-size: 0.95rem; 1152 - } 1153 - 1154 - .docs-nav .next { 1155 - text-align: right; 1156 - margin-left: auto; 1157 - } 1158 - 1159 - /* ------------------------------------------------------- 1160 - Footer 1161 - ------------------------------------------------------- */ 1162 - 1163 - .site-footer { 1164 - border-top: 1px solid var(--color-border); 1165 - padding: 2rem; 1166 - background: var(--color-surface); 1167 - } 1168 - 1169 - .footer-container { 1170 - max-width: var(--max-width); 1171 - margin: 0 auto; 1172 - text-align: center; 1173 - } 1174 - 1175 - .site-footer p { 1176 - color: var(--color-text-muted); 1177 - font-size: 0.85rem; 1178 - margin-bottom: 0.2rem; 1179 - } 1180 - 1181 - .site-footer a { 1182 - color: var(--color-text-muted); 1183 - font-weight: 500; 1184 - } 1185 - 1186 - .site-footer a:hover { 1187 - color: var(--color-accent); 1188 - } 1189 - 1190 - /* ------------------------------------------------------- 1191 - Entrance Animations 1192 - ------------------------------------------------------- */ 1193 - 1194 - @keyframes fade-up { 1195 - from { 1196 - opacity: 0; 1197 - transform: translateY(12px); 1198 - } 1199 - 1200 - to { 1201 - opacity: 1; 1202 - transform: translateY(0); 1203 - } 1204 - } 1205 - 1206 - .hero-content { 1207 - animation: fade-up 0.5s ease-out both; 1208 - } 1209 - 1210 - .hero-code { 1211 - animation: fade-up 0.5s ease-out 0.15s both; 1212 - } 1213 - 1214 - .feature-card { 1215 - animation: fade-up 0.4s ease-out both; 1216 - } 1217 - 1218 - .feature-card:nth-child(1) { 1219 - animation-delay: 0.05s; 1220 - } 1221 - 1222 - .feature-card:nth-child(2) { 1223 - animation-delay: 0.1s; 1224 - } 1225 - 1226 - .feature-card:nth-child(3) { 1227 - animation-delay: 0.15s; 1228 - } 1229 - 1230 - .feature-card:nth-child(4) { 1231 - animation-delay: 0.2s; 1232 - } 1233 - 1234 - .feature-card:nth-child(5) { 1235 - animation-delay: 0.25s; 1236 - } 1237 - 1238 - .feature-card:nth-child(6) { 1239 - animation-delay: 0.3s; 1240 - } 1241 - 1242 - /* ------------------------------------------------------- 1243 - Responsive 1244 - ------------------------------------------------------- */ 1245 - 1246 - @media (max-width: 900px) { 1247 - .hero { 1248 - grid-template-columns: 1fr; 1249 - padding: 2.5rem 1.5rem 3rem; 1250 - } 1251 - 1252 - .hero-code { 1253 - max-width: 520px; 1254 - } 1255 - 1256 - .features-grid { 1257 - grid-template-columns: 1fr; 1258 - } 1259 - 1260 - .feature-card--featured { 1261 - grid-row: auto; 1262 - } 1263 - 1264 - .quickstart-steps { 1265 - grid-template-columns: 1fr; 1266 - } 1267 - 1268 - .docs-container { 1269 - grid-template-columns: 1fr; 1270 - padding: 1.5rem; 1271 - gap: 1.25rem; 1272 - } 1273 - 1274 - .docs-sidebar { 1275 - position: static; 1276 - border-bottom: 1px solid var(--color-border); 1277 - padding-bottom: 0; 1278 - } 1279 - 1280 - .docs-sidebar-menu { 1281 - display: block; 1282 - } 1283 - 1284 - .docs-sidebar-toggle { 1285 - display: block; 1286 - padding: 0.6rem 0; 1287 - font-size: 0.88rem; 1288 - font-weight: 600; 1289 - color: var(--color-text); 1290 - cursor: pointer; 1291 - list-style: none; 1292 - } 1293 - 1294 - .docs-sidebar-toggle::-webkit-details-marker { 1295 - display: none; 1296 - } 1297 - 1298 - .docs-sidebar-toggle::before { 1299 - content: "\25B6"; 1300 - display: inline-block; 1301 - margin-right: 0.5rem; 1302 - font-size: 0.65rem; 1303 - transition: transform 0.15s ease; 1304 - } 1305 - 1306 - .docs-sidebar-menu[open]>.docs-sidebar-toggle::before { 1307 - transform: rotate(90deg); 1308 - } 1309 - 1310 - .docs-sidebar-nav { 1311 - padding-bottom: 1rem; 1312 - } 1313 - 1314 - .hero-title { 1315 - font-size: clamp(1.75rem, 5vw, 2.2rem); 1316 - } 1317 - } 1318 - 1319 - @media (max-width: 600px) { 1320 - .hero-actions { 1321 - flex-direction: column; 1322 - align-items: stretch; 1323 - } 1324 - 1325 - .btn { 1326 - text-align: center; 1327 - } 1328 - 1329 - .guestbook-container { 1330 - padding: 1rem; 1331 - } 1332 - 1333 - .docs-nav { 1334 - flex-direction: column; 1335 - } 1336 - 1337 - .docs-nav .next { 1338 - text-align: left; 1339 - } 1340 - } 1341 - 1342 - /* ------------------------------------------------------- 1343 - Reduced motion 1344 - ------------------------------------------------------- */ 1345 - 1346 - @media (prefers-reduced-motion: reduce) { 1347 - 1348 - *, 1349 - *::before, 1350 - *::after { 1351 - animation-duration: 0.01ms !important; 1352 - animation-delay: 0ms !important; 1353 - transition-duration: 0.01ms !important; 1354 - } 1355 - 1356 - html { 1357 - scroll-behavior: auto; 1358 - } 1359 - } 1 + /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */ 2 + @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"DM Sans", system-ui, -apple-system, sans-serif;--font-mono:"JetBrains Mono", ui-monospace, "SF Mono", monospace;--color-black:#000;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-lg:10px;--animate-spin:spin .6s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--radius:6px;--color-bg:#fffbf7;--color-text:#2c2418;--color-text-muted:#7a6e5d;--color-text-subtle:#a89f90;--color-accent:#e8722a;--color-accent-hover:#d15f1a;--color-accent-light:#fef3eb;--color-accent-warm:#f5a623;--color-border:#e8e0d4;--color-border-light:#f2ece4;--color-surface:#f7f2ec;--color-code-bg:#2a2520;--color-code-text:#e0d6c8;--color-header-bg:#2c2418;--color-header-text:#f7f2ec;--color-hero-bg:#2c2418;--font-display:"Source Serif 4", Georgia, serif;--container-site:1120px}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth;font-size:16px}body{font-family:var(--font-sans);color:var(--color-text);background:var(--color-bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;line-height:1.7}a{color:var(--color-accent);text-decoration:none;transition:color .15s}a:hover{color:var(--color-accent-hover)}code{font-family:var(--font-mono);font-size:.875em}p code,li code,td code,h1 code,h2 code,h3 code{background:var(--color-surface);border:1px solid var(--color-border);color:var(--color-accent-hover);border-radius:4px;padding:.1em .35em;font-size:.84em}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-delay:0s!important}html{scroll-behavior:auto}}}@layer components{.skip-link{background:var(--color-accent);color:#fff;border-radius:var(--radius);z-index:200;padding:.5rem 1rem;font-size:.9rem;font-weight:600;transition:top .15s;position:absolute;top:-100%;left:1rem}.skip-link:focus{color:#fff;top:.5rem}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.spinner{border:2px solid var(--color-border);border-top-color:var(--color-accent);width:16px;height:16px;animation:var(--animate-spin);border-radius:50%}.htmx-indicator{display:none}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:block}.docs-nav{border-top:1px solid var(--color-border);justify-content:space-between;gap:1rem;margin-top:3.5rem;padding-top:1.75rem;display:flex}.docs-nav>a{border:1px solid var(--color-border);border-radius:var(--radius);min-width:170px;padding:.85rem 1.1rem;text-decoration:none;transition:border-color .2s;display:block}.docs-nav>a:hover{border-color:var(--color-accent)}.docs-nav .next{text-align:right;margin-left:auto}.docs-nav-label{font-family:var(--font-mono);color:var(--color-text-subtle);letter-spacing:.04em;margin-bottom:.2rem;font-size:.7rem;font-weight:500}.docs-nav-title{color:var(--color-text);font-size:.95rem;font-weight:600}@media (max-width:600px){.docs-nav{flex-direction:column}.docs-nav .next{text-align:left}}.btn{border-radius:var(--radius);cursor:pointer;font-size:.92rem;font-weight:600;font-family:var(--font-sans);text-align:center;border:1.5px solid #0000;justify-content:center;align-items:center;padding:.65rem 1.4rem;text-decoration:none;transition:all .2s;display:inline-flex}.btn-primary{background:var(--color-accent);color:#fff;border-color:var(--color-accent)}.btn-primary:hover{background:var(--color-accent-hover);border-color:var(--color-accent-hover);color:#fff;transform:translateY(-1px)}.btn-secondary{color:var(--color-text-subtle);background:0 0;border-color:#ffffff26}.btn-secondary:hover{color:var(--color-header-text);border-color:#ffffff59}.btn-sm{padding:.35rem .85rem;font-size:.82rem}.feature-card{background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-lg);padding:clamp(1.5rem,2.5vw,2rem);transition:border-color .2s,box-shadow .2s;animation:.4s ease-out both fade-up}.feature-card:hover{border-color:var(--color-accent);box-shadow:0 2px 12px #e8722a0f}.code-block{border-radius:var(--radius);border:1px solid #2a252014;margin:1.25rem 0;overflow:hidden}.code-block pre,.code-block pre[class*=language-]{background:var(--color-code-bg);margin:0;padding:1.2rem;overflow-x:auto}.code-block pre code{color:var(--color-code-text);font-size:.84rem;line-height:1.65}.code-block code[class*=language-]{background:0 0}.hero-code-window pre,.hero-code-window pre[class*=language-]{background:var(--color-code-bg);margin:0;padding:1.25rem;overflow-x:auto}.hero-code-window pre code{font-size:.84rem;line-height:1.65}.hero-code-window code[class*=language-]{background:0 0}.alert-success{color:#2e7d32;background:#e8f5e9;border:1px solid #c8e6c9}.alert-error{color:#c62828;background:#fef3eb;border:1px solid #ffe0cc}details[open]>summary[data-disclosure]:before{transform:rotate(90deg)}.prose-md pre.mermaid{text-align:center;background:0 0;border:0;margin:1.5rem 0;padding:0;overflow-x:auto}.prose-md pre.mermaid:not([data-processed=true]){visibility:hidden;min-height:6rem}.prose-md pre.mermaid svg{max-width:100%;height:auto}.prose-md{max-width:720px}.prose-md h1{font-family:var(--font-display);letter-spacing:-.02em;margin-bottom:.4rem;font-size:clamp(1.75rem,3vw,2.1rem);font-weight:700;line-height:1.2}.prose-md h1+p{color:var(--color-text-muted);margin-bottom:2rem;font-size:1.05rem;line-height:1.7}.prose-md h2{font-family:var(--font-display);border-top:1px solid var(--color-border-light);letter-spacing:-.01em;margin-top:2.75rem;margin-bottom:.75rem;padding-top:1.5rem;font-size:clamp(1.2rem,2vw,1.35rem);font-weight:700;line-height:1.3}.prose-md h2:first-of-type{border-top:none;margin-top:1.75rem;padding-top:0}.prose-md h3{font-family:var(--font-display);margin-top:1.75rem;margin-bottom:.6rem;font-size:1.05rem;font-weight:700;line-height:1.3}.prose-md p{color:var(--color-text);margin-bottom:1rem}.prose-md ul,.prose-md ol{margin-bottom:1.25rem;padding-left:1.5rem}.prose-md li{color:var(--color-text);margin-bottom:.35rem}.prose-md strong{font-weight:600}.prose-md blockquote{border-left:3px solid var(--color-accent);background:var(--color-accent-light);border-radius:0 var(--radius) var(--radius) 0;margin:1.5rem 0;padding:.7rem 1.2rem}.prose-md blockquote p{color:var(--color-text);margin:0}.prose-md table{border-collapse:collapse;width:100%;margin:1.25rem 0;font-size:.9rem}.prose-md th{text-align:left;background:var(--color-surface);border:1px solid var(--color-border);padding:.6rem .85rem;font-size:.84rem;font-weight:600}.prose-md td{border:1px solid var(--color-border);vertical-align:top;padding:.55rem .85rem}.prose-md pre.chroma{border-radius:var(--radius);background:var(--color-code-bg);color:var(--color-code-text);border:1px solid #2a252014;margin:1.25rem 0;padding:1.2rem;font-size:.84rem;line-height:1.65;overflow-x:auto}.prose-md pre.chroma code{color:inherit;font-size:inherit;background:0 0;padding:0}.chroma .k,.chroma .kd,.chroma .kn,.chroma .kp,.chroma .kr,.chroma .kt{color:#c98dff}.chroma .s,.chroma .sa,.chroma .sb,.chroma .sc,.chroma .s1,.chroma .s2{color:#a5d96b}.chroma .c,.chroma .c1,.chroma .cm,.chroma .cs,.chroma .cp{color:#8a8170;font-style:italic}.chroma .n,.chroma .nx,.chroma .nv{color:var(--color-code-text)}.chroma .nf,.chroma .nc,.chroma .nb{color:#7ec4ff}.chroma .nt{color:#ff9e75}.chroma .na,.chroma .m,.chroma .mi,.chroma .mf{color:#e8a860}.chroma .o,.chroma .p{color:#d6c9b4}.chroma .err{color:#ff8b6e}}@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-\[0\.6rem\]{top:.6rem}.right-3{right:calc(var(--spacing) * 3)}.right-\[0\.6rem\]{right:.6rem}.isolate{isolation:isolate}.z-\[100\]{z-index:100}.container{width:100%}@media (min-width:600px){.container{max-width:600px}}@media (min-width:900px){.container{max-width:900px}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing) * 0)}.mx-auto{margin-inline:auto}.mt-0{margin-top:calc(var(--spacing) * 0)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-\[0\.6rem\]{margin-top:.6rem}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-\[0\.2rem\]{margin-bottom:.2rem}.mb-\[0\.4rem\]{margin-bottom:.4rem}.mb-\[0\.6rem\]{margin-bottom:.6rem}.mb-\[0\.35rem\]{margin-bottom:.35rem}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.list-item{display:list-item}.table{display:table}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-14{height:calc(var(--spacing) * 14)}.h-\[26px\]{height:26px}.min-h-\[80px\]{min-height:80px}.min-h-\[calc\(100vh-156px\)\]{min-height:calc(100vh - 156px)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-full{width:100%}.max-w-\[480px\]{max-width:480px}.max-w-\[520px\]{max-width:520px}.max-w-site{max-width:var(--container-site)}.min-w-0{min-width:calc(var(--spacing) * 0)}.grow{flex-grow:1}.border-collapse{border-collapse:collapse}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-\[fade-up_0\.5s_ease-out_0\.15s_both\]{animation:.5s ease-out .15s both fade-up}.animate-\[fade-up_0\.5s_ease-out_both\]{animation:.5s ease-out both fade-up}.cursor-pointer{cursor:pointer}.resize-y{resize:vertical}.list-none{list-style-type:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-\[auto\]{grid-template-rows:auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-7{gap:calc(var(--spacing) * 7)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-\[0\.6rem\]{gap:.6rem}.self-start{align-self:flex-start}.overflow-hidden{overflow:hidden}.rounded{border-radius:var(--radius)}.rounded-\[4px\]{border-radius:4px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.border{border-style:var(--tw-border-style);border-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-accent{border-color:var(--color-accent)}.border-border{border-color:var(--color-border)}.border-border-light{border-color:var(--color-border-light)}.bg-\[\#5eb86c\]{background-color:#5eb86c}.bg-\[\#e8a73e\]{background-color:#e8a73e}.bg-\[\#e85c4a\]{background-color:#e85c4a}.bg-accent-light{background-color:var(--color-accent-light)}.bg-bg{background-color:var(--color-bg)}.bg-black\/25{background-color:#00000040}@supports (color:color-mix(in lab, red, red)){.bg-black\/25{background-color:color-mix(in oklab, var(--color-black) 25%, transparent)}}.bg-code-bg{background-color:var(--color-code-bg)}.bg-header-bg{background-color:var(--color-header-bg)}.bg-hero-bg{background-color:var(--color-hero-bg)}.bg-surface{background-color:var(--color-surface)}.bg-transparent{background-color:#0000}.p-0{padding:calc(var(--spacing) * 0)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-\[0\.85rem_1rem\]{padding:.85rem 1rem}.p-\[clamp\(2rem\,3vw\,2\.5rem\)\]{padding:clamp(2rem,3vw,2.5rem)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-\[0\.85rem\]{padding-inline:.85rem}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-\[0\.2rem\]{padding-block:.2rem}.py-\[0\.6rem\]{padding-block:.6rem}.py-\[0\.7rem\]{padding-block:.7rem}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-10{padding-right:calc(var(--spacing) * 10)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.text-center{text-align:center}.font-display{font-family:var(--font-display)}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-\[0\.7rem\]{font-size:.7rem}.text-\[0\.9rem\]{font-size:.9rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[0\.78rem\]{font-size:.78rem}.text-\[0\.82rem\]{font-size:.82rem}.text-\[0\.85rem\]{font-size:.85rem}.text-\[0\.88rem\]{font-size:.88rem}.text-\[0\.93rem\]{font-size:.93rem}.text-\[1\.1rem\]{font-size:1.1rem}.text-\[1\.05rem\]{font-size:1.05rem}.text-\[clamp\(1\.2rem\,2vw\,1\.4rem\)\]{font-size:clamp(1.2rem,2vw,1.4rem)}.text-\[clamp\(1\.6rem\,3vw\,2\.2rem\)\]{font-size:clamp(1.6rem,3vw,2.2rem)}.text-\[clamp\(1\.75rem\,5vw\,2\.2rem\)\]{font-size:clamp(1.75rem,5vw,2.2rem)}.leading-\[1\.2\]{--tw-leading:1.2;line-height:1.2}.leading-\[1\.3\]{--tw-leading:1.3;line-height:1.3}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-\[1\.6\]{--tw-leading:1.6;line-height:1.6}.leading-\[1\.7\]{--tw-leading:1.7;line-height:1.7}.leading-\[1\.15\]{--tw-leading:1.15;line-height:1.15}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[-0\.02em\]{--tw-tracking:-.02em;letter-spacing:-.02em}.tracking-\[0\.01em\]{--tw-tracking:.01em;letter-spacing:.01em}.tracking-\[0\.02em\]{--tw-tracking:.02em;letter-spacing:.02em}.tracking-\[0\.04em\]{--tw-tracking:.04em;letter-spacing:.04em}.tracking-\[0\.08em\]{--tw-tracking:.08em;letter-spacing:.08em}.whitespace-nowrap{white-space:nowrap}.text-accent{color:var(--color-accent)}.text-accent-light{color:var(--color-accent-light)}.text-accent-warm{color:var(--color-accent-warm)}.text-header-text{color:var(--color-header-text)}.text-text{color:var(--color-text)}.text-text-muted{color:var(--color-text-muted)}.text-text-subtle{color:var(--color-text-subtle)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.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-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[animation-delay\:0\.1s\]{animation-delay:.1s}.\[animation-delay\:0\.2s\]{animation-delay:.2s}.\[animation-delay\:0\.3s\]{animation-delay:.3s}.\[animation-delay\:0\.05s\]{animation-delay:50ms}.\[animation-delay\:0\.15s\]{animation-delay:.15s}.\[animation-delay\:0\.25s\]{animation-delay:.25s}.\[animation-delay\:0\.35s\]{animation-delay:.35s}@media (hover:hover){.group-hover\/entry\:opacity-100:is(:where(.group\/entry):hover *){opacity:1}}.placeholder\:text-text-subtle::placeholder{color:var(--color-text-subtle)}.before\:mr-2:before{content:var(--tw-content);margin-right:calc(var(--spacing) * 2)}.before\:inline-block:before{content:var(--tw-content);display:inline-block}.before\:text-\[0\.65rem\]:before{content:var(--tw-content);font-size:.65rem}.before\:transition-transform:before{content:var(--tw-content);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.before\:content-\[\'\\25B6\'\]:before{--tw-content:"▶";content:var(--tw-content)}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--color-accent)}.hover\:text-accent:hover{color:var(--color-accent)}}.focus\:border-accent:focus{border-color:var(--color-accent)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media not all and (min-width:900px){.max-md\:block{display:block}.max-md\:max-w-\[520px\]{max-width:520px}.max-md\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.max-md\:border-border{border-color:var(--color-border)}.max-md\:pb-0{padding-bottom:calc(var(--spacing) * 0)}.max-md\:pb-4{padding-bottom:calc(var(--spacing) * 4)}}@media not all and (min-width:600px){.max-sm\:flex-col{flex-direction:column}.max-sm\:items-stretch{align-items:stretch}}@media (min-width:600px){.sm\:p-\[clamp\(1\.25rem\,2\.5vw\,2rem\)\]{padding:clamp(1.25rem,2.5vw,2rem)}}@media (min-width:900px){.md\:sticky{position:sticky}.md\:top-\[72px\]{top:72px}.md\:row-span-2{grid-row:span 2/span 2}.md\:mb-\[clamp\(2rem\,3vw\,3rem\)\]{margin-bottom:clamp(2rem,3vw,3rem)}.md\:mb-\[clamp\(2rem\,4vw\,3\.5rem\)\]{margin-bottom:clamp(2rem,4vw,3.5rem)}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-\[1\.1fr_0\.9fr\]{grid-template-columns:1.1fr .9fr}.md\:grid-cols-\[210px_1fr\]{grid-template-columns:210px 1fr}.md\:grid-rows-\[auto_auto_auto\]{grid-template-rows:auto auto auto}.md\:gap-12{gap:calc(var(--spacing) * 12)}.md\:gap-\[clamp\(2rem\,4vw\,5rem\)\]{gap:clamp(2rem,4vw,5rem)}.md\:self-start{align-self:flex-start}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}.md\:py-8{padding-block:calc(var(--spacing) * 8)}.md\:py-\[clamp\(3\.5rem\,6vw\,5\.5rem\)\]{padding-block:clamp(3.5rem,6vw,5.5rem)}.md\:py-\[clamp\(3rem\,6vw\,6rem\)\]{padding-block:clamp(3rem,6vw,6rem)}.md\:pt-3{padding-top:calc(var(--spacing) * 3)}.md\:text-\[clamp\(1rem\,1\.5vw\,1\.1rem\)\]{font-size:clamp(1rem,1.5vw,1.1rem)}.md\:text-\[clamp\(2rem\,4vw\,3rem\)\]{font-size:clamp(2rem,4vw,3rem)}}.\[\&_a\]\:block a{display:block}.\[\&_a\]\:rounded-\[5px\] a{border-radius:5px}.\[\&_a\]\:px-\[0\.7rem\] a{padding-inline:.7rem}.\[\&_a\]\:py-\[0\.3rem\] a{padding-block:.3rem}.\[\&_a\]\:text-\[0\.88rem\] a{font-size:.88rem}.\[\&_a\]\:font-medium a{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.\[\&_a\]\:text-text-muted a{color:var(--color-text-muted)}.\[\&_a\]\:text-text-subtle a{color:var(--color-text-subtle)}.\[\&_a\]\:transition-all a{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[\&_a\]\:transition-colors a{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[\&_a\:hover\]\:bg-surface a:hover{background-color:var(--color-surface)}.\[\&_a\:hover\]\:text-accent a:hover{color:var(--color-accent)}.\[\&_a\:hover\]\:text-header-text a:hover{color:var(--color-header-text)}.\[\&_a\:hover\]\:text-text a:hover{color:var(--color-text)}.\[\&_li\]\:mb-\[0\.15rem\] li{margin-bottom:.15rem}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}.\[\&\>\*\:first-child\]\:col-span-full>:first-child{grid-column:1/-1}.shadow-code-window{box-shadow:0 20px 40px -12px #2c241873,0 0 0 1px #ffffff0a}.ring-accent-soft{box-shadow:0 0 0 3px #e8722a1a}.ring-accent-soft-light{box-shadow:0 0 0 3px #e8722a14}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes fade-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
+549
examples/gastro/tailwind.css
··· 1 + /* Tailwind v4 source for the gastro-website example. 2 + Compiled into static/styles.css via the //go:generate directive in 3 + generate.go. Tokens migrated 1:1 from the previous hand-rolled 4 + styles.css :root block — the warm cream / muted brown / accent orange 5 + palette is preserved verbatim. 6 + 7 + Markdown-rendered content (md.MustRender output) is wrapped in 8 + <div class="prose-md"> by the docs-layout component, and the prose 9 + styles below provide the typography for those subtrees. 10 + 11 + Mermaid theme JS in components/layout.gastro hard-codes the same hex 12 + literals declared in @theme below — keep both in sync if the palette 13 + ever changes. (This duplication exists because the mermaid client 14 + library can't read CSS custom properties at init time.) 15 + */ 16 + 17 + @import "tailwindcss"; 18 + 19 + /* Where to scan for utility class names. The .gastro template files are 20 + the source of truth; helpers.go is included in case Go-side code ever 21 + injects classes into rendered HTML. */ 22 + @source "./components/**/*.gastro"; 23 + @source "./pages/**/*.gastro"; 24 + @source "./*.go"; 25 + 26 + @theme { 27 + /* Palette ---------------------------------------------------------- */ 28 + --color-bg: #fffbf7; 29 + --color-text: #2c2418; 30 + --color-text-muted: #7a6e5d; 31 + --color-text-subtle: #a89f90; 32 + --color-accent: #e8722a; 33 + --color-accent-hover: #d15f1a; 34 + --color-accent-light: #fef3eb; 35 + --color-accent-warm: #f5a623; 36 + --color-border: #e8e0d4; 37 + --color-border-light: #f2ece4; 38 + --color-surface: #f7f2ec; 39 + --color-code-bg: #2a2520; 40 + --color-code-text: #e0d6c8; 41 + --color-header-bg: #2c2418; 42 + --color-header-text: #f7f2ec; 43 + --color-hero-bg: #2c2418; 44 + 45 + /* Typography ------------------------------------------------------- */ 46 + --font-display: "Source Serif 4", Georgia, serif; 47 + --font-sans: "DM Sans", system-ui, -apple-system, sans-serif; 48 + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace; 49 + 50 + /* Radius ----------------------------------------------------------- */ 51 + --radius: 6px; 52 + --radius-lg: 10px; 53 + 54 + /* Container width — site is 1120px wide. Exposed as `max-w-site`. -- */ 55 + --container-site: 1120px; 56 + 57 + /* Breakpoints -------------------------------------------------------- 58 + The original styles.css used hand-rolled media queries at 900px and 59 + 600px. We override Tailwind's default `sm:` and `md:` breakpoints to 60 + those exact values so templates can stay mobile-first and use plain 61 + `sm:` / `md:` modifiers without falling back to arbitrary 62 + `[@media(max-width:900px)]:` strings everywhere. */ 63 + --breakpoint-sm: 600px; 64 + --breakpoint-md: 900px; 65 + 66 + /* Custom keyframes -------------------------------------------------- */ 67 + --animate-fade-up: fade-up 0.5s ease-out both; 68 + --animate-spin: spin 0.6s linear infinite; 69 + 70 + @keyframes fade-up { 71 + from { opacity: 0; transform: translateY(12px); } 72 + to { opacity: 1; transform: translateY(0); } 73 + } 74 + 75 + @keyframes spin { 76 + to { transform: rotate(360deg); } 77 + } 78 + } 79 + 80 + /* ------------------------------------------------------------------- 81 + Base layer — global element defaults that aren't tied to a single 82 + utility composition. Keep this minimal; everything that *can* live as 83 + a utility on a template element should. 84 + ------------------------------------------------------------------- */ 85 + @layer base { 86 + *, 87 + *::before, 88 + *::after { 89 + margin: 0; 90 + padding: 0; 91 + box-sizing: border-box; 92 + } 93 + 94 + html { 95 + font-size: 16px; 96 + scroll-behavior: smooth; 97 + } 98 + 99 + body { 100 + font-family: var(--font-sans); 101 + line-height: 1.7; 102 + color: var(--color-text); 103 + background: var(--color-bg); 104 + -webkit-font-smoothing: antialiased; 105 + -moz-osx-font-smoothing: grayscale; 106 + } 107 + 108 + a { 109 + color: var(--color-accent); 110 + text-decoration: none; 111 + transition: color 0.15s ease; 112 + } 113 + 114 + a:hover { color: var(--color-accent-hover); } 115 + 116 + code { 117 + font-family: var(--font-mono); 118 + font-size: 0.875em; 119 + } 120 + 121 + /* Inline code inside copy gets a chip treatment. The wider .prose-md 122 + scope handles the same selectors for markdown-rendered content. */ 123 + p code, li code, td code, h1 code, h2 code, h3 code { 124 + background: var(--color-surface); 125 + border: 1px solid var(--color-border); 126 + padding: 0.1em 0.35em; 127 + border-radius: 4px; 128 + font-size: 0.84em; 129 + color: var(--color-accent-hover); 130 + } 131 + 132 + /* Reduced-motion users opt out of all animation. */ 133 + @media (prefers-reduced-motion: reduce) { 134 + *, *::before, *::after { 135 + animation-duration: 0.01ms !important; 136 + animation-delay: 0ms !important; 137 + transition-duration: 0.01ms !important; 138 + } 139 + html { scroll-behavior: auto; } 140 + } 141 + } 142 + 143 + /* ------------------------------------------------------------------- 144 + Component layer — multi-element patterns that don't decompose cleanly 145 + into utilities. Kept deliberately small. 146 + ------------------------------------------------------------------- */ 147 + @layer components { 148 + /* Skip link is hidden offscreen until focused (keyboard / SR users). */ 149 + .skip-link { 150 + position: absolute; 151 + top: -100%; 152 + left: 1rem; 153 + padding: 0.5rem 1rem; 154 + background: var(--color-accent); 155 + color: white; 156 + border-radius: var(--radius); 157 + font-weight: 600; 158 + font-size: 0.9rem; 159 + z-index: 200; 160 + transition: top 0.15s ease; 161 + } 162 + .skip-link:focus { top: 0.5rem; color: white; } 163 + 164 + /* sr-only — screen-reader-only text. (Tailwind ships `sr-only` as a 165 + utility but keeping this here means template authors can use the 166 + same class regardless of source.) */ 167 + .sr-only { 168 + position: absolute; 169 + width: 1px; height: 1px; 170 + padding: 0; margin: -1px; 171 + overflow: hidden; 172 + clip: rect(0, 0, 0, 0); 173 + white-space: nowrap; 174 + border: 0; 175 + } 176 + 177 + /* Spinner — used inside both Datastar and HTMX loading indicators. */ 178 + .spinner { 179 + width: 16px; height: 16px; 180 + border: 2px solid var(--color-border); 181 + border-top-color: var(--color-accent); 182 + border-radius: 50%; 183 + animation: var(--animate-spin); 184 + } 185 + 186 + /* HTMX visibility contract — htmx.js toggles the `htmx-request` class 187 + on the source element while a request is in flight, and any 188 + descendant marked `htmx-indicator` becomes visible. We invert the 189 + default `display:none` from Tailwind reset by re-asserting both 190 + sides of the contract. */ 191 + .htmx-indicator { display: none; } 192 + .htmx-request .htmx-indicator, 193 + .htmx-request.htmx-indicator { display: block; } 194 + 195 + /* Docs prev/next nav. Pattern: a flex row of one or two anchors, 196 + each containing a small label + larger title, with the second 197 + anchor right-aligned via the `.next` modifier. Used in every docs 198 + page and guestbook example — 11 call sites, so it earns its keep 199 + as a component class instead of an 11-fold utility chain repeat. */ 200 + .docs-nav { 201 + display: flex; 202 + justify-content: space-between; 203 + margin-top: 3.5rem; 204 + padding-top: 1.75rem; 205 + border-top: 1px solid var(--color-border); 206 + gap: 1rem; 207 + } 208 + .docs-nav > a { 209 + display: block; 210 + padding: 0.85rem 1.1rem; 211 + border: 1px solid var(--color-border); 212 + border-radius: var(--radius); 213 + transition: border-color 0.2s ease; 214 + min-width: 170px; 215 + text-decoration: none; 216 + } 217 + .docs-nav > a:hover { border-color: var(--color-accent); } 218 + .docs-nav .next { text-align: right; margin-left: auto; } 219 + .docs-nav-label { 220 + font-family: var(--font-mono); 221 + font-size: 0.7rem; 222 + color: var(--color-text-subtle); 223 + font-weight: 500; 224 + letter-spacing: 0.04em; 225 + margin-bottom: 0.2rem; 226 + } 227 + .docs-nav-title { 228 + font-weight: 600; 229 + color: var(--color-text); 230 + font-size: 0.95rem; 231 + } 232 + @media (max-width: 600px) { 233 + .docs-nav { flex-direction: column; } 234 + .docs-nav .next { text-align: left; } 235 + } 236 + 237 + /* Buttons ------------------------------------------------------- 238 + `.btn` is the structural reset (padding, radius, transitions); the 239 + two variants pin foreground/background/border. Used heavily across 240 + the hero, docs nav, and guestbook examples — expressing this as 241 + utilities every time would be ~12 classes per call site. */ 242 + .btn { 243 + display: inline-flex; 244 + align-items: center; 245 + justify-content: center; 246 + padding: 0.65rem 1.4rem; 247 + border-radius: var(--radius); 248 + font-size: 0.92rem; 249 + font-weight: 600; 250 + text-decoration: none; 251 + transition: all 0.2s ease; 252 + border: 1.5px solid transparent; 253 + cursor: pointer; 254 + font-family: var(--font-sans); 255 + text-align: center; 256 + } 257 + .btn-primary { 258 + background: var(--color-accent); 259 + color: white; 260 + border-color: var(--color-accent); 261 + } 262 + .btn-primary:hover { 263 + background: var(--color-accent-hover); 264 + border-color: var(--color-accent-hover); 265 + color: white; 266 + transform: translateY(-1px); 267 + } 268 + .btn-secondary { 269 + background: transparent; 270 + color: var(--color-text-subtle); 271 + border-color: rgba(255, 255, 255, 0.15); 272 + } 273 + .btn-secondary:hover { 274 + border-color: rgba(255, 255, 255, 0.35); 275 + color: var(--color-header-text); 276 + } 277 + .btn-sm { 278 + padding: 0.35rem 0.85rem; 279 + font-size: 0.82rem; 280 + } 281 + 282 + /* Feature cards on the home page. Six instances with staggered 283 + entrance animations — the `[animation-delay:Xs]` utility on each 284 + instance feeds the shared `fade-up` keyframe defined in @theme. */ 285 + .feature-card { 286 + background: var(--color-bg); 287 + border: 1px solid var(--color-border); 288 + border-radius: var(--radius-lg); 289 + padding: clamp(1.5rem, 2.5vw, 2rem); 290 + transition: border-color 0.2s ease, box-shadow 0.2s ease; 291 + animation: fade-up 0.4s ease-out both; 292 + } 293 + .feature-card:hover { 294 + border-color: var(--color-accent); 295 + box-shadow: 0 2px 12px rgba(232, 114, 42, 0.06); 296 + } 297 + 298 + /* Code blocks (docs) ---------------------------------------------- 299 + Wraps PrismJS-highlighted <pre> blocks in docs pages. The inner 300 + selectors override Prism's theme background so it inherits our 301 + dark code background instead of the CDN theme's slate gray. */ 302 + .code-block { 303 + margin: 1.25rem 0; 304 + border-radius: var(--radius); 305 + overflow: hidden; 306 + border: 1px solid rgba(42, 37, 32, 0.08); 307 + } 308 + .code-block pre, 309 + .code-block pre[class*="language-"] { 310 + margin: 0; 311 + padding: 1.2rem; 312 + overflow-x: auto; 313 + background: var(--color-code-bg); 314 + } 315 + .code-block pre code { 316 + font-size: 0.84rem; 317 + line-height: 1.65; 318 + color: var(--color-code-text); 319 + } 320 + .code-block code[class*="language-"] { 321 + background: transparent; 322 + } 323 + 324 + /* Hero code window <pre> overrides — the IntroHero markdown render 325 + injects PrismJS-classed elements we don't control. Selecting them 326 + under .hero-code-window keeps the rules local. */ 327 + .hero-code-window pre, 328 + .hero-code-window pre[class*="language-"] { 329 + margin: 0; 330 + padding: 1.25rem; 331 + overflow-x: auto; 332 + background: var(--color-code-bg); 333 + } 334 + .hero-code-window pre code { 335 + font-size: 0.84rem; 336 + line-height: 1.65; 337 + } 338 + .hero-code-window code[class*="language-"] { 339 + background: transparent; 340 + } 341 + 342 + /* Guestbook alerts ------------------------------------------------- 343 + Standalone color schemes that don't compose well from utilities 344 + (the success palette uses Material green, error uses our accent 345 + warm cream + Material red). */ 346 + .alert-success { 347 + background: #e8f5e9; 348 + color: #2e7d32; 349 + border: 1px solid #c8e6c9; 350 + } 351 + .alert-error { 352 + background: #fef3eb; 353 + color: #c62828; 354 + border: 1px solid #ffe0cc; 355 + } 356 + 357 + /* Docs sidebar disclosure arrow. The arrow itself is rendered via 358 + `before:content-['\25B6']` utilities on the <summary> in 359 + docs-layout.gastro; this rule handles the rotate-on-open transition 360 + because parent-state selectors (`details[open] > summary::before`) 361 + don't express cleanly as utility variants. */ 362 + details[open] > summary[data-disclosure]::before { 363 + transform: rotate(90deg); 364 + } 365 + 366 + /* Mermaid — hide raw source until mermaid.js has swapped in the SVG. */ 367 + .prose-md pre.mermaid { 368 + margin: 1.5rem 0; 369 + padding: 0; 370 + background: transparent; 371 + border: 0; 372 + text-align: center; 373 + overflow-x: auto; 374 + } 375 + .prose-md pre.mermaid:not([data-processed="true"]) { 376 + visibility: hidden; 377 + min-height: 6rem; 378 + } 379 + .prose-md pre.mermaid svg { 380 + max-width: 100%; 381 + height: auto; 382 + } 383 + } 384 + 385 + /* ------------------------------------------------------------------- 386 + Markdown prose styles — applied via <div class="prose-md"> wrapper 387 + around md.MustRender output. Kept minimal and hand-rolled (no 388 + @tailwindcss/typography plugin, per AGENTS.md 3rd-party policy). 389 + ------------------------------------------------------------------- */ 390 + @layer components { 391 + .prose-md { 392 + max-width: 720px; 393 + } 394 + 395 + .prose-md h1 { 396 + font-family: var(--font-display); 397 + font-size: clamp(1.75rem, 3vw, 2.1rem); 398 + font-weight: 700; 399 + margin-bottom: 0.4rem; 400 + letter-spacing: -0.02em; 401 + line-height: 1.2; 402 + } 403 + /* Lede paragraph immediately after the page title gets a softer treatment. */ 404 + .prose-md h1 + p { 405 + font-size: 1.05rem; 406 + color: var(--color-text-muted); 407 + margin-bottom: 2rem; 408 + line-height: 1.7; 409 + } 410 + 411 + .prose-md h2 { 412 + font-family: var(--font-display); 413 + font-size: clamp(1.2rem, 2vw, 1.35rem); 414 + font-weight: 700; 415 + margin-top: 2.75rem; 416 + margin-bottom: 0.75rem; 417 + padding-top: 1.5rem; 418 + border-top: 1px solid var(--color-border-light); 419 + letter-spacing: -0.01em; 420 + line-height: 1.3; 421 + } 422 + .prose-md h2:first-of-type { 423 + margin-top: 1.75rem; 424 + border-top: none; 425 + padding-top: 0; 426 + } 427 + 428 + .prose-md h3 { 429 + font-family: var(--font-display); 430 + font-size: 1.05rem; 431 + font-weight: 700; 432 + margin-top: 1.75rem; 433 + margin-bottom: 0.6rem; 434 + line-height: 1.3; 435 + } 436 + 437 + .prose-md p { 438 + margin-bottom: 1rem; 439 + color: var(--color-text); 440 + } 441 + 442 + .prose-md ul, 443 + .prose-md ol { 444 + margin-bottom: 1.25rem; 445 + padding-left: 1.5rem; 446 + } 447 + 448 + .prose-md li { 449 + margin-bottom: 0.35rem; 450 + color: var(--color-text); 451 + } 452 + 453 + .prose-md strong { font-weight: 600; } 454 + 455 + .prose-md blockquote { 456 + border-left: 3px solid var(--color-accent); 457 + margin: 1.5rem 0; 458 + padding: 0.7rem 1.2rem; 459 + background: var(--color-accent-light); 460 + border-radius: 0 var(--radius) var(--radius) 0; 461 + } 462 + .prose-md blockquote p { margin: 0; color: var(--color-text); } 463 + 464 + /* Tables --------------------------------------------------------- */ 465 + .prose-md table { 466 + width: 100%; 467 + border-collapse: collapse; 468 + margin: 1.25rem 0; 469 + font-size: 0.9rem; 470 + } 471 + .prose-md th { 472 + text-align: left; 473 + padding: 0.6rem 0.85rem; 474 + background: var(--color-surface); 475 + border: 1px solid var(--color-border); 476 + font-weight: 600; 477 + font-size: 0.84rem; 478 + } 479 + .prose-md td { 480 + padding: 0.55rem 0.85rem; 481 + border: 1px solid var(--color-border); 482 + vertical-align: top; 483 + } 484 + 485 + /* Chroma syntax highlighting (markdown code blocks). 486 + Site-specific muted palette tuned for the dark code background. 487 + Independent of the `gastro new` scaffold's github theme. */ 488 + .prose-md pre.chroma { 489 + margin: 1.25rem 0; 490 + padding: 1.2rem; 491 + border-radius: var(--radius); 492 + border: 1px solid rgba(42, 37, 32, 0.08); 493 + overflow-x: auto; 494 + background: var(--color-code-bg); 495 + color: var(--color-code-text); 496 + font-size: 0.84rem; 497 + line-height: 1.65; 498 + } 499 + .prose-md pre.chroma code { 500 + background: transparent; 501 + color: inherit; 502 + padding: 0; 503 + font-size: inherit; 504 + } 505 + 506 + /* Token colors — keyword */ 507 + .chroma .k, .chroma .kd, .chroma .kn, .chroma .kp, .chroma .kr, .chroma .kt { color: #c98dff; } 508 + /* string */ 509 + .chroma .s, .chroma .sa, .chroma .sb, .chroma .sc, .chroma .s1, .chroma .s2 { color: #a5d96b; } 510 + /* comment */ 511 + .chroma .c, .chroma .c1, .chroma .cm, .chroma .cs { color: #8a8170; font-style: italic; } 512 + /* preproc — used for gastro frontmatter --- and {{ ... }} */ 513 + .chroma .cp { color: #8a8170; font-style: italic; } 514 + /* names / variables */ 515 + .chroma .n, .chroma .nx, .chroma .nv { color: var(--color-code-text); } 516 + /* function / component names */ 517 + .chroma .nf, .chroma .nc { color: #7ec4ff; } 518 + /* builtin */ 519 + .chroma .nb { color: #7ec4ff; } 520 + /* HTML tag name */ 521 + .chroma .nt { color: #ff9e75; } 522 + /* HTML / component attribute name */ 523 + .chroma .na { color: #e8a860; } 524 + /* numbers */ 525 + .chroma .m, .chroma .mi, .chroma .mf { color: #e8a860; } 526 + /* operators / punctuation */ 527 + .chroma .o, .chroma .p { color: #d6c9b4; } 528 + .chroma .err { color: #ff8b6e; } 529 + } 530 + 531 + /* ------------------------------------------------------------------- 532 + Utilities layer — small helpers that are easier to express as a 533 + single class name than as a long utility chain repeated everywhere. 534 + ------------------------------------------------------------------- */ 535 + @layer utilities { 536 + /* Code window framing in the hero — matches `.code-window` shadow. */ 537 + .shadow-code-window { 538 + box-shadow: 0 20px 40px -12px rgba(44, 36, 24, 0.45), 539 + 0 0 0 1px rgba(255, 255, 255, 0.04); 540 + } 541 + 542 + /* Accent focus ring used on guestbook inputs. */ 543 + .ring-accent-soft { 544 + box-shadow: 0 0 0 3px rgba(232, 114, 42, 0.1); 545 + } 546 + .ring-accent-soft-light { 547 + box-shadow: 0 0 0 3px rgba(232, 114, 42, 0.08); 548 + } 549 + }
+4
mise.toml
··· 3 3 node = "lts" 4 4 "go:golang.org/x/tools/gopls" = "latest" 5 5 "npm:@playwright/cli" = "latest" 6 + # Standalone Tailwind v4 CLI binary used to compile examples/gastro/tailwind.css 7 + # into examples/gastro/static/styles.css. Pinned exactly so go generate output 8 + # stays reproducible across machines and CI. See examples/gastro/generate.go. 9 + "github:tailwindlabs/tailwindcss" = "v4.3.0" 6 10 7 11 [task_config] 8 12 includes = ["scripts"]
+25
scripts/dev/example-website
··· 1 + #!/usr/bin/env bash 2 + #MISE description="Run examples/gastro with Tailwind CSS rebuild on change" 3 + 4 + # Hot-reload loop for the gastro-website example. 5 + # 6 + # tailwindcss rebuilds static/styles.css when tailwind.css or 7 + # any scanned .gastro file changes 8 + # go build rebuilds the binary against the regenerated 9 + # .gastro/ package after gastro watch's own codegen 10 + # gastro watch owns the .gastro -> .gastro/ codegen step itself, 11 + # which is why we don't list `gastro generate` as a 12 + # separate --build (it would double-run). 13 + # 14 + # We invoke tailwindcss without --minify here so dev rebuilds stay fast 15 + # and the un-minified output is easier to read in the browser inspector. 16 + # The committed static/styles.css is the *minified* output produced by 17 + # `go generate ./...` (see generate.go). 18 + 19 + set -euo pipefail 20 + cd "$(dirname "$0")/../../examples/gastro" 21 + 22 + exec gastro watch \ 23 + --build 'tailwindcss -i tailwind.css -o static/styles.css' \ 24 + --build 'go build -o tmp/app .' \ 25 + --run 'tmp/app'