Commit c196720

mo khan <mo@mokhan.ca>
2026-01-31 03:05:18
refactor: extract css files
1 parent fcba44c
cmd/gitmal/main.go
@@ -107,6 +107,10 @@ func main() {
 
 	generator.Echo(fmt.Sprintf("> %s: %d branches, %d tags, %d commits", params.Name, len(branches), len(tags), len(commits)))
 
+	if err := generator.GenerateCSS(params); err != nil {
+		panic(err)
+	}
+
 	if err := generator.GenerateBranches(branches, defaultBranch, params); err != nil {
 		panic(err)
 	}
internal/generator/blob.go
@@ -34,7 +34,6 @@ func GenerateBlobs(files []git.Blob, params Params) error {
 		html.WithCSSComments(false),
 	}
 
-	css := cssSyntax(formatterOptions)
 	dirsSet := links.BuildDirSet(files)
 	filesSet := links.BuildFileSet(files)
 
@@ -92,12 +91,12 @@ func GenerateBlobs(files []git.Blob, params Params) error {
 
 				return templates.MarkdownTemplate.ExecuteTemplate(f, "layout.gohtml", templates.MarkdownParams{
 					LayoutParams: templates.LayoutParams{
-						Title:         fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
-						CSSMarkdown:   cssMarkdown(),
-						Name:          params.Name,
-						RootHref:      rootHref,
-						CurrentRefDir: params.Ref.DirName(),
-						Selected:      "code",
+						Title:            fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
+						Name:             params.Name,
+						RootHref:         rootHref,
+						CurrentRefDir:    params.Ref.DirName(),
+						Selected:         "code",
+						NeedsMarkdownCSS: true,
 					},
 					HeaderParams: templates.HeaderParams{
 						Ref:         params.Ref,
@@ -145,17 +144,17 @@ func GenerateBlobs(files []git.Blob, params Params) error {
 
 			return templates.BlobTemplate.ExecuteTemplate(f, "layout.gohtml", templates.BlobParams{
 				LayoutParams: templates.LayoutParams{
-					Title:         fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
-					Name:          params.Name,
-					RootHref:      rootHref,
-					CurrentRefDir: params.Ref.DirName(),
-					Selected:      "code",
+					Title:          fmt.Sprintf("%s/%s at %s", params.Name, blob.Path, params.Ref),
+					Name:           params.Name,
+					RootHref:       rootHref,
+					CurrentRefDir:  params.Ref.DirName(),
+					Selected:       "code",
+					NeedsSyntaxCSS: !isBin,
 				},
 				HeaderParams: templates.HeaderParams{
 					Ref:         params.Ref,
 					Breadcrumbs: breadcrumbs(params.Name, blob.Path, true),
 				},
-				CSS:      css,
 				Blob:     blob,
 				IsBinary: isBin,
 				IsImage:  isImg,
@@ -165,21 +164,3 @@ func GenerateBlobs(files []git.Blob, params Params) error {
 	)
 }
 
-func cssSyntax(opts []html.Option) template.CSS {
-	formatter := html.New(opts...)
-	lightStyle := styles.Get("github")
-	darkStyle := styles.Get("github-dark")
-
-	var light, dark strings.Builder
-	_ = formatter.WriteCSS(&light, lightStyle)
-	_ = formatter.WriteCSS(&dark, darkStyle)
-
-	return template.CSS(
-		"@media (prefers-color-scheme: light) {\n" +
-			light.String() +
-			"\n}\n" +
-			"@media (prefers-color-scheme: dark) {\n" +
-			dark.String() +
-			"\n}",
-	)
-}
internal/generator/commit.go
@@ -9,7 +9,6 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/alecthomas/chroma/v2"
 	"github.com/alecthomas/chroma/v2/formatters/html"
 	"github.com/alecthomas/chroma/v2/lexers"
 	"github.com/alecthomas/chroma/v2/styles"
@@ -50,26 +49,9 @@ func generateCommitPage(commit git.Commit, params Params) error {
 	formatter := html.New(
 		html.WithClasses(true),
 		html.WithCSSComments(false),
-		html.WithCustomCSS(map[chroma.TokenType]string{
-			chroma.GenericInserted: "display: block;",
-			chroma.GenericDeleted:  "display: block;",
-		}),
 	)
 
 	lightStyle := styles.Get("github")
-	darkStyle := styles.Get("github-dark")
-
-	var lightCSS, darkCSS bytes.Buffer
-	_ = formatter.WriteCSS(&lightCSS, lightStyle)
-	_ = formatter.WriteCSS(&darkCSS, darkStyle)
-
-	diffCSS := "@media (prefers-color-scheme: light) {\n" +
-		lightCSS.String() +
-		"\n}\n" +
-		"@media (prefers-color-scheme: dark) {\n" +
-		darkCSS.String() +
-		"\n}"
-
 	lexer := lexers.Get("diff")
 	if lexer == nil {
 		return fmt.Errorf("failed to get lexer for diff")
@@ -171,14 +153,14 @@ func generateCommitPage(commit git.Commit, params Params) error {
 
 	err = templates.CommitTemplate.ExecuteTemplate(f, "layout.gohtml", templates.CommitParams{
 		LayoutParams: templates.LayoutParams{
-			Title:         fmt.Sprintf("%s %s %s@%s", commit.Subject, Dot, params.Name, commit.ShortHash),
-			Name:          params.Name,
-			RootHref:      rootHref,
-			CurrentRefDir: currentRef.DirName(),
-			Selected:      "commits",
+			Title:          fmt.Sprintf("%s %s %s@%s", commit.Subject, Dot, params.Name, commit.ShortHash),
+			Name:           params.Name,
+			RootHref:       rootHref,
+			CurrentRefDir:  currentRef.DirName(),
+			Selected:       "commits",
+			NeedsSyntaxCSS: true,
 		},
 		Commit:    commit,
-		DiffCSS:   template.CSS(diffCSS),
 		FileTree:  fileTree,
 		FileViews: filesViews,
 	})
internal/generator/css.go
@@ -0,0 +1,86 @@
+package generator
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/alecthomas/chroma/v2"
+	"github.com/alecthomas/chroma/v2/formatters/html"
+	"github.com/alecthomas/chroma/v2/styles"
+
+	"mokhan.ca/antonmedv/gitmal/internal/templates"
+)
+
+func GenerateCSS(params Params) error {
+	cssDir := filepath.Join(params.OutputDir, "css")
+	if err := os.MkdirAll(cssDir, 0o755); err != nil {
+		return err
+	}
+
+	if err := writeLayoutCSS(cssDir); err != nil {
+		return err
+	}
+	if err := writeMarkdownCSS(cssDir); err != nil {
+		return err
+	}
+	if err := writeSyntaxCSS(cssDir); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func writeLayoutCSS(cssDir string) error {
+	return os.WriteFile(filepath.Join(cssDir, "layout.css"), []byte(templates.LayoutCSS), 0o644)
+}
+
+func writeMarkdownCSS(cssDir string) error {
+	css := "@media (prefers-color-scheme: light) {\n" +
+		templates.CSSMarkdownLight +
+		"\n}\n" +
+		"@media (prefers-color-scheme: dark) {\n" +
+		templates.CSSMarkdownDark +
+		"\n}"
+	return os.WriteFile(filepath.Join(cssDir, "markdown.css"), []byte(css), 0o644)
+}
+
+func writeSyntaxCSS(cssDir string) error {
+	formatterOptions := []html.Option{
+		html.WithLineNumbers(true),
+		html.WithLinkableLineNumbers(true, "L"),
+		html.WithClasses(true),
+		html.WithCSSComments(false),
+	}
+
+	formatter := html.New(formatterOptions...)
+	lightStyle := styles.Get("github")
+	darkStyle := styles.Get("github-dark")
+
+	var light, dark strings.Builder
+	_ = formatter.WriteCSS(&light, lightStyle)
+	_ = formatter.WriteCSS(&dark, darkStyle)
+
+	diffFormatter := html.New(
+		html.WithClasses(true),
+		html.WithCSSComments(false),
+		html.WithCustomCSS(map[chroma.TokenType]string{
+			chroma.GenericInserted: "display: block;",
+			chroma.GenericDeleted:  "display: block;",
+		}),
+	)
+	var diffLight, diffDark strings.Builder
+	_ = diffFormatter.WriteCSS(&diffLight, lightStyle)
+	_ = diffFormatter.WriteCSS(&diffDark, darkStyle)
+
+	css := "@media (prefers-color-scheme: light) {\n" +
+		light.String() +
+		diffLight.String() +
+		"\n}\n" +
+		"@media (prefers-color-scheme: dark) {\n" +
+		dark.String() +
+		diffDark.String() +
+		"\n}"
+
+	return os.WriteFile(filepath.Join(cssDir, "syntax.css"), []byte(css), 0o644)
+}
internal/generator/index.go
@@ -100,12 +100,12 @@ func GenerateIndex(files []git.Blob, params Params) error {
 
 	err = templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
 		LayoutParams: templates.LayoutParams{
-			Title:         title,
-			Name:          params.Name,
-			CSSMarkdown:   cssMarkdown(),
-			RootHref:      rootHref,
-			CurrentRefDir: params.Ref.DirName(),
-			Selected:      "code",
+			Title:            title,
+			Name:             params.Name,
+			RootHref:         rootHref,
+			CurrentRefDir:    params.Ref.DirName(),
+			Selected:         "code",
+			NeedsMarkdownCSS: readmeHTML != "",
 		},
 		HeaderParams: templates.HeaderParams{
 			Ref:         params.Ref,
internal/generator/list.go
@@ -2,7 +2,6 @@ package generator
 
 import (
 	"fmt"
-	"html/template"
 	"os"
 	"path/filepath"
 	"sort"
@@ -127,19 +126,15 @@ func GenerateLists(files []git.Blob, params Params) error {
 		rootHref := strings.Repeat("../", depth+2)
 
 		readmeHTML := readme(di.files, dirsSet, filesSet, params, rootHref)
-		var CSSMarkdown template.CSS
-		if readmeHTML != "" {
-			CSSMarkdown = cssMarkdown()
-		}
 
 		return templates.ListTemplate.ExecuteTemplate(f, "layout.gohtml", templates.ListParams{
 			LayoutParams: templates.LayoutParams{
-				Title:         title,
-				Name:          params.Name,
-				CSSMarkdown:   CSSMarkdown,
-				RootHref:      rootHref,
-				CurrentRefDir: params.Ref.DirName(),
-				Selected:      "code",
+				Title:            title,
+				Name:             params.Name,
+				RootHref:         rootHref,
+				CurrentRefDir:    params.Ref.DirName(),
+				Selected:         "code",
+				NeedsMarkdownCSS: readmeHTML != "",
 			},
 			HeaderParams: templates.HeaderParams{
 				Ref:         params.Ref,
internal/generator/markdown.go
@@ -1,15 +1,11 @@
 package generator
 
 import (
-	"html/template"
-
 	"github.com/yuin/goldmark"
 	highlighting "github.com/yuin/goldmark-highlighting/v2"
 	"github.com/yuin/goldmark/extension"
 	"github.com/yuin/goldmark/parser"
 	gmhtml "github.com/yuin/goldmark/renderer/html"
-
-	"mokhan.ca/antonmedv/gitmal/internal/templates"
 )
 
 func createMarkdown() goldmark.Markdown {
@@ -29,14 +25,3 @@ func createMarkdown() goldmark.Markdown {
 		),
 	)
 }
-
-func cssMarkdown() template.CSS {
-	return template.CSS(
-		"@media (prefers-color-scheme: light) {\n" +
-			templates.CSSMarkdownLight +
-			"\n}\n" +
-			"@media (prefers-color-scheme: dark) {\n" +
-			templates.CSSMarkdownDark +
-			"\n}",
-	)
-}
internal/templates/css/layout.css
@@ -0,0 +1,257 @@
+:root {
+  --c-indigo-1: #3451b2;
+  --c-indigo-2: #3a5ccc;
+  --c-indigo-3: #5672cd;
+  --c-green: #1a7f37;
+  --c-red: #c53030;
+  --c-yellow: #9a6700;
+  --c-dir: #54aeff;
+  --c-gray-soft: rgba(142, 150, 170, .14);
+  --c-bg: #ffffff;
+  --c-bg-alt: #f6f6f7;
+  --c-bg-elv: #ffffff;
+  --c-text-1: rgba(60, 60, 67);
+  --c-text-2: rgba(60, 60, 67, .78);
+  --c-text-3: rgba(60, 60, 67, .56);
+  --c-border: #c2c2c4;
+  --c-divider: #e2e2e3;
+}
+
+@media (prefers-color-scheme: dark) {
+  :root {
+    --c-indigo-1: #a8b1ff;
+    --c-indigo-2: #5c73e7;
+    --c-indigo-3: #3e63dd;
+    --c-green: #57ab5a;
+    --c-red: #e5534b;
+    --c-yellow: #c69026;
+    --c-dir: #9198a1;
+    --c-gray-soft: rgba(101, 117, 133, .16);
+    --c-bg: #1b1b1f;
+    --c-bg-alt: #161618;
+    --c-bg-elv: #202127;
+    --c-text-1: rgba(255, 255, 245, .86);
+    --c-text-2: rgba(235, 235, 245, .6);
+    --c-text-3: rgba(235, 235, 245, .38);
+    --c-border: #3c3f44;
+    --c-divider: #2e2e32;
+  }
+}
+
+:root {
+  --c-brand-1: var(--c-indigo-1);
+  --c-brand-2: var(--c-indigo-2);
+  --c-brand-3: var(--c-indigo-3);
+  --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+  --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+  --code-line-height: 20px;
+  --code-font-size: 12px;
+  --code-color: var(--c-brand-1);
+  --code-bg: var(--c-gray-soft);
+  --code-block-bg: var(--c-bg-alt);
+  --code-block-color: var(--c-text-1);
+  --header-height: 46px;
+  --border-radius: 6px;
+  --max-content-width: 1470px;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  font-family: var(--font-family), sans-serif;
+  font-size: 14px;
+  line-height: 1;
+  color: var(--c-text-1);
+  background-color: var(--c-bg);
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+  -moz-text-size-adjust: none;
+  -webkit-text-size-adjust: none;
+  text-size-adjust: none;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.nowrap {
+  white-space: nowrap;
+}
+
+h1 {
+  margin-inline: 0;
+  margin-block: 16px;
+  font-size: 20px;
+  font-weight: 600;
+}
+
+a {
+  color: var(--c-brand-1);
+  font-weight: 500;
+  text-decoration: underline;
+  text-underline-offset: 2px;
+  text-decoration: inherit;
+  touch-action: manipulation;
+}
+
+a:hover {
+  color: var(--c-brand-2);
+  text-decoration: underline;
+}
+
+.menu {
+  background-color: var(--c-bg-alt);
+  border-bottom: 1px solid var(--c-divider);
+  overflow-x: auto;
+}
+
+.menu-content {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  gap: 16px;
+  padding-inline: 16px;
+  max-width: var(--max-content-width);
+  margin-inline: auto;
+}
+
+.menu-item {
+  display: flex;
+  align-items: center;
+  border-bottom: 2px solid transparent;
+  height: 56px;
+  padding-inline: 8px;
+}
+
+.menu-item a {
+  display: flex;
+  flex-direction: row;
+  gap: 8px;
+  align-items: center;
+  color: var(--c-text-1);
+  padding: 8px 10px;
+  border-radius: 4px;
+}
+
+.menu-item a:hover {
+  background-color: var(--c-bg-elv);
+  text-decoration: none;
+}
+
+.menu-item.selected {
+  border-bottom-color: var(--c-brand-1);
+}
+
+.project-name {
+  font-weight: 600;
+  font-size: 16px;
+  margin-inline: 16px;
+  color: var(--c-text-1);
+  text-decoration: none;
+}
+
+main {
+  flex-grow: 1;
+  width: 100%;
+  max-width: var(--max-content-width);
+  margin: 16px auto;
+}
+
+.main-content {
+  padding-inline: 16px;
+}
+
+footer {
+  padding: 12px 16px;
+  background-color: var(--c-bg-alt);
+  border-top: 1px solid var(--c-divider);
+  color: var(--c-text-3);
+  font-size: 12px;
+  text-align: center;
+}
+
+.header-container {
+  container-type: scroll-state;
+  position: sticky;
+  top: 0;
+}
+
+.header-container {
+  @container scroll-state(stuck: top) {
+    header {
+      border-top: none;
+      border-top-left-radius: 0;
+      border-top-right-radius: 0;
+    }
+
+    .goto-top {
+      display: flex;
+    }
+  }
+}
+
+header {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  min-height: var(--header-height);
+  padding-inline: 16px;
+  background: var(--c-bg-alt);
+  border: 1px solid var(--c-border);
+  border-top-left-radius: var(--border-radius);
+  border-top-right-radius: var(--border-radius);
+}
+
+header h1 {
+  word-break: break-all;
+  font-weight: 600;
+  font-size: 16px;
+  margin: 0;
+  padding: 0;
+}
+
+.header-ref {
+  color: var(--c-text-2);
+  border: 1px solid var(--c-border);
+  border-radius: 6px;
+  padding: 6px 10px;
+  margin-right: 10px;
+  margin-left: -6px;
+}
+
+header .path {
+  font-size: 16px;
+}
+
+.breadcrumbs {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 6px;
+  font-size: 16px;
+}
+
+.breadcrumbs a {
+  word-break: break-all;
+}
+
+.goto-top {
+  display: none;
+  margin-left: auto;
+  padding: 6px 10px;
+  background: none;
+  border: none;
+  border-radius: 6px;
+  gap: 4px;
+  align-items: center;
+  color: var(--c-text-1);
+  cursor: pointer;
+}
+
+.goto-top:hover {
+  background: var(--c-bg-elv);
+}
internal/templates/blob.gohtml
@@ -1,8 +1,6 @@
 {{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.BlobParams */ -}}
 {{ define "head" }}
     <style>
-        {{ .CSS }}
-
         [id] {
           scroll-margin-top: var(--header-height);
         }
internal/templates/commit.gohtml
@@ -214,8 +214,6 @@
         border-bottom-left-radius: 6px;
         border-bottom-right-radius: 6px;
       }
-
-      {{ .DiffCSS }}
     </style>
 {{ end }}
 
internal/templates/layout.gohtml
@@ -5,265 +5,9 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>{{ .Title }}</title>
-    <style>
-        :root {
-          --c-indigo-1: #3451b2;
-          --c-indigo-2: #3a5ccc;
-          --c-indigo-3: #5672cd;
-          --c-green: #1a7f37;
-          --c-red: #c53030;
-          --c-yellow: #9a6700;
-          --c-dir: #54aeff;
-          --c-gray-soft: rgba(142, 150, 170, .14);
-          --c-bg: #ffffff;
-          --c-bg-alt: #f6f6f7;
-          --c-bg-elv: #ffffff;
-          --c-text-1: rgba(60, 60, 67);
-          --c-text-2: rgba(60, 60, 67, .78);
-          --c-text-3: rgba(60, 60, 67, .56);
-          --c-border: #c2c2c4;
-          --c-divider: #e2e2e3;
-        }
-
-        @media (prefers-color-scheme: dark) {
-          :root {
-            --c-indigo-1: #a8b1ff;
-            --c-indigo-2: #5c73e7;
-            --c-indigo-3: #3e63dd;
-            --c-green: #57ab5a;
-            --c-red: #e5534b;
-            --c-yellow: #c69026;
-            --c-dir: #9198a1;
-            --c-gray-soft: rgba(101, 117, 133, .16);
-            --c-bg: #1b1b1f;
-            --c-bg-alt: #161618;
-            --c-bg-elv: #202127;
-            --c-text-1: rgba(255, 255, 245, .86);
-            --c-text-2: rgba(235, 235, 245, .6);
-            --c-text-3: rgba(235, 235, 245, .38);
-            --c-border: #3c3f44;
-            --c-divider: #2e2e32;
-          }
-        }
-
-        :root {
-          --c-brand-1: var(--c-indigo-1);
-          --c-brand-2: var(--c-indigo-2);
-          --c-brand-3: var(--c-indigo-3);
-          --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-          --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
-          --code-line-height: 20px;
-          --code-font-size: 12px;
-          --code-color: var(--c-brand-1);
-          --code-bg: var(--c-gray-soft);
-          --code-block-bg: var(--c-bg-alt);
-          --code-block-color: var(--c-text-1);
-          --header-height: 46px;
-          --border-radius: 6px;
-          --max-content-width: 1470px;
-        }
-
-        * {
-          box-sizing: border-box;
-        }
-
-        body {
-          margin: 0;
-          padding: 0;
-          font-family: var(--font-family), sans-serif;
-          font-size: 14px;
-          line-height: 1;
-          color: var(--c-text-1);
-          background-color: var(--c-bg);
-          text-rendering: optimizeLegibility;
-          -webkit-font-smoothing: antialiased;
-          -moz-osx-font-smoothing: grayscale;
-          -moz-text-size-adjust: none;
-          -webkit-text-size-adjust: none;
-          text-size-adjust: none;
-          min-height: 100vh;
-          display: flex;
-          flex-direction: column;
-        }
-
-        .nowrap {
-          white-space: nowrap;
-        }
-
-        h1 {
-          margin-inline: 0;
-          margin-block: 16px;
-          font-size: 20px;
-          font-weight: 600;
-        }
-
-        a {
-          color: var(--c-brand-1);
-          font-weight: 500;
-          text-decoration: underline;
-          text-underline-offset: 2px;
-          text-decoration: inherit;
-          touch-action: manipulation;
-        }
-
-        a:hover {
-          color: var(--c-brand-2);
-          text-decoration: underline;
-        }
-
-        .menu {
-          background-color: var(--c-bg-alt);
-          border-bottom: 1px solid var(--c-divider);
-          overflow-x: auto;
-        }
-
-        .menu-content {
-          display: flex;
-          flex-direction: row;
-          align-items: center;
-          gap: 16px;
-          padding-inline: 16px;
-          max-width: var(--max-content-width);
-          margin-inline: auto;
-        }
-
-        .menu-item {
-          display: flex;
-          align-items: center;
-          border-bottom: 2px solid transparent;
-          height: 56px;
-          padding-inline: 8px;
-        }
-
-        .menu-item a {
-          display: flex;
-          flex-direction: row;
-          gap: 8px;
-          align-items: center;
-          color: var(--c-text-1);
-          padding: 8px 10px;
-          border-radius: 4px;
-        }
-
-        .menu-item a:hover {
-          background-color: var(--c-bg-elv);
-          text-decoration: none;
-        }
-
-        .menu-item.selected {
-          border-bottom-color: var(--c-brand-1);
-        }
-
-        .project-name {
-          font-weight: 600;
-          font-size: 16px;
-          margin-inline: 16px;
-          color: var(--c-text-1);
-          text-decoration: none;
-        }
-
-        main {
-          flex-grow: 1;
-          width: 100%;
-          max-width: var(--max-content-width);
-          margin: 16px auto;
-        }
-
-        .main-content {
-          padding-inline: 16px;
-        }
-
-        footer {
-          padding: 12px 16px;
-          background-color: var(--c-bg-alt);
-          border-top: 1px solid var(--c-divider);
-          color: var(--c-text-3);
-          font-size: 12px;
-          text-align: center;
-        }
-
-        .header-container {
-          container-type: scroll-state;
-          position: sticky;
-          top: 0;
-        }
-
-        .header-container {
-          @container scroll-state(stuck: top) {
-            header {
-              border-top: none;
-              border-top-left-radius: 0;
-              border-top-right-radius: 0;
-            }
-
-            .goto-top {
-              display: flex;
-            }
-          }
-        }
-
-        header {
-          display: flex;
-          flex-direction: row;
-          align-items: center;
-          min-height: var(--header-height);
-          padding-inline: 16px;
-          background: var(--c-bg-alt);
-          border: 1px solid var(--c-border);
-          border-top-left-radius: var(--border-radius);
-          border-top-right-radius: var(--border-radius);
-        }
-
-        header h1 {
-          word-break: break-all;
-          font-weight: 600;
-          font-size: 16px;
-          margin: 0;
-          padding: 0;
-        }
-
-        .header-ref {
-          color: var(--c-text-2);
-          border: 1px solid var(--c-border);
-          border-radius: 6px;
-          padding: 6px 10px;
-          margin-right: 10px;
-          margin-left: -6px;
-        }
-
-        header .path {
-          font-size: 16px;
-        }
-
-        .breadcrumbs {
-          display: flex;
-          flex-direction: row;
-          flex-wrap: wrap;
-          gap: 6px;
-          font-size: 16px;
-        }
-
-        .breadcrumbs a {
-          word-break: break-all;
-        }
-
-        .goto-top {
-          display: none;
-          margin-left: auto;
-          padding: 6px 10px;
-          background: none;
-          border: none;
-          border-radius: 6px;
-          gap: 4px;
-          align-items: center;
-          color: var(--c-text-1);
-          cursor: pointer;
-        }
-
-        .goto-top:hover {
-          background: var(--c-bg-elv);
-        }
-    </style>
+    <link rel="stylesheet" href="{{ .RootHref }}css/layout.css">
+    {{- if .NeedsMarkdownCSS }}<link rel="stylesheet" href="{{ .RootHref }}css/markdown.css">{{ end }}
+    {{- if .NeedsSyntaxCSS }}<link rel="stylesheet" href="{{ .RootHref }}css/syntax.css">{{ end }}
     {{ template "head" . }}
 </head>
 <body>
internal/templates/markdown.gohtml
@@ -29,8 +29,6 @@
           padding: 16px;
         }
       }
-
-      {{.CSSMarkdown}}
     </style>
 {{ end }}
 
internal/templates/svg.gohtml
@@ -1,50 +1,38 @@
 {{ define "svg" }}
-    <svg style="display: none" aria-hidden="true" focusable="false">
-        <symbol id="dir" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/>
-        </symbol>
-        <symbol id="file" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/>
-        </symbol>
-        <symbol id="file-added" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.48 3.758a.75.75 0 0 1 .755.745l.01 1.497h1.497a.75.75 0 0 1 0 1.5H9v1.507a.75.75 0 0 1-1.5 0V9.005l-1.502.01a.75.75 0 0 1-.01-1.5l1.507-.01-.01-1.492a.75.75 0 0 1 .745-.755Z"/>
-        </symbol>
-        <symbol id="file-modified" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"/>
-        </symbol>
-        <symbol id="file-deleted" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.5 6h2.242a.75.75 0 0 1 0 1.5h-2.24l-2.254.015a.75.75 0 0 1-.01-1.5Z"/>
-        </symbol>
-        <symbol id="file-renamed" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-3.5a.75.75 0 0 1 0-1.5h3.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073H3.75a.25.25 0 0 0-.25.25v6.5a.75.75 0 0 1-1.5 0v-6.5Z"/>
-            <path fill="currentColor"
-                  d="m5.427 15.573 3.146-3.146a.25.25 0 0 0 0-.354L5.427 8.927A.25.25 0 0 0 5 9.104V11.5H.75a.75.75 0 0 0 0 1.5H5v2.396c0 .223.27.335.427.177Z"/>
-        </symbol>
-        <symbol id="arrow-top" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z"></path>
-        </symbol>
-        <symbol id="commit" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path>
-        </symbol>
-        <symbol id="branch" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"></path>
-        </symbol>
-        <symbol id="tag" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
-        </symbol>
-        <symbol id="code" viewBox="0 0 16 16">
-            <path fill="currentColor"
-                  d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"></path>
-        </symbol>
-    </svg>
+<svg style="display: none" aria-hidden="true" focusable="false">
+  <symbol id="dir" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/>
+  </symbol>
+  <symbol id="file" viewBox="0 0 16 16">
+      <path fill="currentColor" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/>
+  </symbol>
+  <symbol id="file-added" viewBox="0 0 16 16">
+      <path fill="currentColor" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.48 3.758a.75.75 0 0 1 .755.745l.01 1.497h1.497a.75.75 0 0 1 0 1.5H9v1.507a.75.75 0 0 1-1.5 0V9.005l-1.502.01a.75.75 0 0 1-.01-1.5l1.507-.01-.01-1.492a.75.75 0 0 1 .745-.755Z"/>
+  </symbol>
+  <symbol id="file-modified" viewBox="0 0 16 16">
+      <path fill="currentColor" d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"/>
+  </symbol>
+  <symbol id="file-deleted" viewBox="0 0 16 16">
+      <path fill="currentColor" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.5 6h2.242a.75.75 0 0 1 0 1.5h-2.24l-2.254.015a.75.75 0 0 1-.01-1.5Z"/>
+  </symbol>
+  <symbol id="file-renamed" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-3.5a.75.75 0 0 1 0-1.5h3.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073H3.75a.25.25 0 0 0-.25.25v6.5a.75.75 0 0 1-1.5 0v-6.5Z"/>
+    <path fill="currentColor" d="m5.427 15.573 3.146-3.146a.25.25 0 0 0 0-.354L5.427 8.927A.25.25 0 0 0 5 9.104V11.5H.75a.75.75 0 0 0 0 1.5H5v2.396c0 .223.27.335.427.177Z"/>
+  </symbol>
+  <symbol id="arrow-top" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z"></path>
+  </symbol>
+  <symbol id="commit" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path>
+  </symbol>
+  <symbol id="branch" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"></path>
+  </symbol>
+  <symbol id="tag" viewBox="0 0 16 16">
+    <path fill="currentColor" d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
+  </symbol>
+  <symbol id="code" viewBox="0 0 16 16">
+    <path fill="currentColor" d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"></path>
+  </symbol>
+</svg>
 {{ end }}
internal/templates/templates.go
@@ -23,6 +23,9 @@ var funcs = FuncMap{
 	},
 }
 
+//go:embed css/layout.css
+var LayoutCSS string
+
 //go:embed css/markdown_light.css
 var CSSMarkdownLight string
 
@@ -62,12 +65,13 @@ var commitContent string
 var CommitTemplate = Must(Must(layout.Clone()).Parse(commitContent))
 
 type LayoutParams struct {
-	Title         string
-	Name          string
-	CSSMarkdown   CSS
-	RootHref      string
-	CurrentRefDir string
-	Selected      string
+	Title            string
+	Name             string
+	RootHref         string
+	CurrentRefDir    string
+	Selected         string
+	NeedsMarkdownCSS bool
+	NeedsSyntaxCSS   bool
 }
 
 type HeaderParams struct {
@@ -85,7 +89,6 @@ type Breadcrumb struct {
 type BlobParams struct {
 	LayoutParams
 	HeaderParams
-	CSS      CSS
 	Blob     git.Blob
 	IsImage  bool
 	IsBinary bool
@@ -154,7 +157,6 @@ type CommitsListParams struct {
 type CommitParams struct {
 	LayoutParams
 	Commit    git.Commit
-	DiffCSS   CSS
 	FileTree  []*FileTree
 	FileViews []FileView
 }