Commit f52df1c

HPCesia <me@hpcesia.com>
2026-07-01 17:38:11
Extract common inline styles into separate CSS files
Default behavior writes layout CSS, markdown CSS, chroma CSS, and diff CSS to external files (style.css, markdown.css, chroma.css, diff.css). Use --inline-styles to keep all CSS inlined in <style> tags (old behavior). The --minify flag now also minifies these CSS files. CSS source of truth lives in Go constants (styles.go) referenced by both the template (via FuncMap) and the file generator, eliminating duplication.
1 parent a108f3a
pkg/templates/blob.gohtml
@@ -1,8 +1,11 @@
 {{- /*gotype: github.com/antonmedv/gitmal/pkg/templates.BlobParams */ -}}
 {{ define "head" }}
+    {{ if .InlineStyles }}
+    <style>{{ .CSS }}
+    {{ else }}
+    <link rel="stylesheet" href="{{ .RootHref }}chroma.css">
     <style>
-        {{ .CSS }}
-
+    {{ end }}
         [id] {
           scroll-margin-top: var(--header-height);
         }
pkg/templates/commit.gohtml
@@ -1,5 +1,6 @@
 {{- /*gotype: github.com/antonmedv/gitmal/pkg/templates.CommitParams*/ -}}
 {{ define "head" }}
+    {{ if not .InlineStyles }}<link rel="stylesheet" href="{{ .RootHref }}diff.css">{{ end }}
     <style>
       h1 code {
         border-radius: var(--border-radius);
@@ -215,7 +216,7 @@
         border-bottom-right-radius: 6px;
       }
 
-      {{ .DiffCSS }}
+      {{ if .InlineStyles }}{{ .DiffCSS }}{{ end }}
     </style>
 {{ end }}
 
pkg/templates/layout.gohtml
@@ -1,271 +1,15 @@
 {{- /*gotype: github.com/antonmedv/gitmal/pkg/templates.LayoutParams*/ -}}
 <!DOCTYPE html>
-<html lang="en">
+<html lang="en"{{ if not .InlineStyles }}{{ if .Dark }} data-theme="dark"{{ end }}{{ end }}>
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>{{ .Title }}</title>
-    <style>
-        {{ if .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;
-        }
-
-        {{ else }}
-        :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;
-        }
-
-        {{ end }}
-
-        :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>
+    {{ if .InlineStyles }}
+    <style>{{ LayoutCSSInline .Dark }}</style>
+    {{ else }}
+    <link rel="stylesheet" href="{{ .RootHref }}style.css">
+    {{ end }}
     {{ template "head" . }}
 </head>
 <body>
pkg/templates/list.gohtml
@@ -1,5 +1,6 @@
 {{- /*gotype: github.com/antonmedv/gitmal/pkg/templates.ListParams*/ -}}
 {{ define "head" }}
+    {{ if and .Readme (not .InlineStyles) }}<link rel="stylesheet" href="{{ .RootHref }}markdown.css">{{ end }}
     <style>
       .files {
         border: 1px solid var(--c-border);
@@ -83,7 +84,7 @@
         }
       }
 
-      {{.CSSMarkdown}}
+      {{ if $.InlineStyles }}{{$.CSSMarkdown}}{{ end }}
       {{ end }}
     </style>
 {{ end }}
pkg/templates/markdown.gohtml
@@ -1,5 +1,12 @@
 {{- /*gotype: github.com/antonmedv/gitmal/pkg/templates.MarkdownParams*/ -}}
 {{ define "head" }}
+    {{ if .InlineStyles }}
+    <style>
+      {{.CSSMarkdown}}
+    </style>
+    {{ else }}
+    <link rel="stylesheet" href="{{ .RootHref }}markdown.css">
+    {{ end }}
     <style>
       [id] {
         scroll-margin-top: var(--header-height);
@@ -29,8 +36,6 @@
           padding: 16px;
         }
       }
-
-      {{.CSSMarkdown}}
     </style>
 {{ end }}
 
pkg/templates/styles.go
@@ -0,0 +1,317 @@
+package templates
+
+import . "html/template"
+
+// Layout CSS constants โ€” these must be kept in sync with the inline CSS
+// block in layout.gohtml (used when --inline-styles is set). Both files
+// define the same rules; styles.go adds a [data-theme="dark"] variant
+// for the external style.css file.
+
+const LayoutCSSVarLight = `: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;
+}
+`
+
+const LayoutCSSVarDarkInline = `: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;
+}
+`
+
+const LayoutCSSVarDarkCombo = `[data-theme="dark"] {
+  --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;
+}
+`
+
+const LayoutCSSShared = `
+: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;
+}
+
+@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;
+}
+
+.raw-button {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 12px;
+  margin-left: auto;
+  font-size: 13px;
+  font-weight: 500;
+  line-height: 20px;
+  color: var(--c-text-1);
+  background: var(--c-bg);
+  border: 1px solid var(--c-border);
+  border-radius: 6px;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+.raw-button:hover {
+  background: var(--c-bg-elv);
+}
+
+.goto-top {
+  display: none;
+  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);
+}
+`
+
+func LayoutCSSCombo() string {
+	return LayoutCSSVarLight + LayoutCSSVarDarkCombo + LayoutCSSShared
+}
+
+func LayoutCSSInline(dark bool) CSS {
+	if dark {
+		return CSS(LayoutCSSVarDarkInline + LayoutCSSShared)
+	}
+	return CSS(LayoutCSSVarLight + LayoutCSSShared)
+}
pkg/templates/templates.go
@@ -21,6 +21,7 @@ var funcs = FuncMap{
 	"FileTreeParams": func(node []*FileTree) FileTreeParams {
 		return FileTreeParams{Nodes: node}
 	},
+	"LayoutCSSInline": LayoutCSSInline,
 }
 
 //go:embed css/markdown_light.css
@@ -73,6 +74,7 @@ type LayoutParams struct {
 	RootHref      string
 	CurrentRefDir string
 	Selected      string
+	InlineStyles  bool
 }
 
 type HeaderParams struct {
blob.go
@@ -141,6 +141,7 @@ func generateBlobs(files []git.Blob, params Params) error {
 								RootHref:      rootHref,
 								CurrentRefDir: params.Ref.DirName(),
 								Selected:      "code",
+								InlineStyles:  params.InlineStyles,
 							},
 							HeaderParams: templates.HeaderParams{
 								Ref:         params.Ref,
@@ -199,6 +200,7 @@ func generateBlobs(files []git.Blob, params Params) error {
 								RootHref:      rootHref,
 								CurrentRefDir: params.Ref.DirName(),
 								Selected:      "code",
+								InlineStyles:  params.InlineStyles,
 							},
 							HeaderParams: templates.HeaderParams{
 								Ref:         params.Ref,
branches.go
@@ -54,6 +54,7 @@ func generateBranches(branches []git.Ref, defaultBranch string, params Params) e
 			RootHref:      rootHref,
 			CurrentRefDir: params.DefaultRef.DirName(),
 			Selected:      "branches",
+			InlineStyles:  params.InlineStyles,
 		},
 		Branches: entries,
 	})
commit.go
@@ -242,6 +242,7 @@ func generateCommitPage(commit git.Commit, params Params) error {
 			RootHref:      rootHref,
 			CurrentRefDir: currentRef.DirName(),
 			Selected:      "commits",
+			InlineStyles:  params.InlineStyles,
 		},
 		Commit:    commit,
 		DiffCSS:   template.CSS(cssBuf.String()),
commits_list.go
@@ -68,6 +68,7 @@ func generateLogForBranch(allCommits []git.Commit, params Params) error {
 				RootHref:      rootHref,
 				CurrentRefDir: params.Ref.DirName(),
 				Selected:      "commits",
+				InlineStyles:  params.InlineStyles,
 			},
 			HeaderParams: templates.HeaderParams{
 				Header: "Commits",
index.go
@@ -107,6 +107,7 @@ func generateIndex(files []git.Blob, params Params) error {
 			RootHref:      rootHref,
 			CurrentRefDir: params.Ref.DirName(),
 			Selected:      "code",
+			InlineStyles:  params.InlineStyles,
 		},
 		HeaderParams: templates.HeaderParams{
 			Ref:         params.Ref,
list.go
@@ -190,6 +190,7 @@ func generateLists(files []git.Blob, params Params) error {
 							RootHref:      rootHref,
 							CurrentRefDir: params.Ref.DirName(),
 							Selected:      "code",
+							InlineStyles:  params.InlineStyles,
 						},
 						HeaderParams: templates.HeaderParams{
 							Ref:         params.Ref,
main.go
@@ -24,17 +24,19 @@ var (
 	flagMinify        bool
 	flagGzip          bool
 	flagGit           bool
+	flagInlineStyles  bool
 )
 
 type Params struct {
-	Owner      string
-	Name       string
-	RepoDir    string
-	Ref        git.Ref
-	OutputDir  string
-	Style      string
-	Dark       bool
-	DefaultRef git.Ref
+	Owner        string
+	Name         string
+	RepoDir      string
+	Ref          git.Ref
+	OutputDir    string
+	Style        string
+	Dark         bool
+	DefaultRef   git.Ref
+	InlineStyles bool
 }
 
 func main() {
@@ -70,6 +72,7 @@ func main() {
 	flag.BoolVar(&flagMinify, "minify", false, "Minify all generated HTML files")
 	flag.BoolVar(&flagGzip, "gzip", false, "Compress all generated HTML files")
 	flag.BoolVar(&flagGit, "git", false, "Generate static files for Git dumb HTTP protocol")
+	flag.BoolVar(&flagInlineStyles, "inline-styles", false, "Keep all CSS inline in HTML instead of external files")
 	flag.Usage = usage
 	flag.Parse()
 
@@ -148,13 +151,20 @@ func main() {
 	// Start generating pages
 
 	params := Params{
-		Owner:      flagOwner,
-		Name:       flagName,
-		RepoDir:    input,
-		OutputDir:  outputDir,
-		Style:      flagTheme,
-		Dark:       themeColor == "dark",
-		DefaultRef: git.NewRef(flagDefaultBranch),
+		Owner:        flagOwner,
+		Name:         flagName,
+		RepoDir:      input,
+		OutputDir:    outputDir,
+		Style:        flagTheme,
+		Dark:         themeColor == "dark",
+		DefaultRef:   git.NewRef(flagDefaultBranch),
+		InlineStyles: flagInlineStyles,
+	}
+
+	if !params.InlineStyles {
+		if err := generateCSSFiles(params); err != nil {
+			panic(err)
+		}
 	}
 
 	commits := make(map[string]git.Commit)
post_process.go
@@ -20,7 +20,7 @@ import (
 )
 
 func postProcessHTML(root string, doMinify bool, doGzip bool) error {
-	// 1) Collect all HTML files first
+	// 1) Collect all HTML and CSS files
 	var files []string
 	if err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
 		if err != nil {
@@ -31,6 +31,8 @@ func postProcessHTML(root string, doMinify bool, doGzip bool) error {
 		}
 		if strings.HasSuffix(d.Name(), ".html") {
 			files = append(files, path)
+		} else if doMinify && strings.HasSuffix(d.Name(), ".css") {
+			files = append(files, path)
 		}
 		return nil
 	}); err != nil {
@@ -74,14 +76,23 @@ func postProcessHTML(root string, doMinify bool, doGzip bool) error {
 		for path := range jobs {
 			data, err := os.ReadFile(path)
 			if err == nil && doMinify {
-				if md, e := m.Bytes("text/html", data); e == nil {
+				mime := "text/html"
+				if strings.HasSuffix(path, ".css") {
+					mime = "text/css"
+				}
+				if md, e := m.Bytes(mime, data); e == nil {
 					data = md
 				} else {
 					err = e
 				}
 			}
 			if err == nil {
-				if doGzip {
+				if strings.HasSuffix(path, ".css") {
+					// CSS files: only minify, no gzip
+					if e := os.WriteFile(path, data, 0o644); e != nil {
+						err = e
+					}
+				} else if doGzip {
 					// write to file.html.gz
 					gzPath := path + ".gz"
 					if e := writeGzip(gzPath, data); e != nil {
styles_gen.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/alecthomas/chroma/v2"
+	"github.com/alecthomas/chroma/v2/formatters/html"
+	"github.com/alecthomas/chroma/v2/styles"
+
+	"github.com/antonmedv/gitmal/pkg/templates"
+)
+
+func generateCSSFiles(params Params) error {
+	out := params.OutputDir
+
+	if err := os.MkdirAll(out, 0o755); err != nil {
+		return err
+	}
+
+	if err := os.WriteFile(filepath.Join(out, "style.css"), []byte(templates.LayoutCSSCombo()), 0o644); err != nil {
+		return err
+	}
+
+	// markdown.css for the active theme
+	var mdCSS string
+	if params.Dark {
+		mdCSS = templates.CSSMarkdownDark
+	} else {
+		mdCSS = templates.CSSMarkdownLight
+	}
+	if err := os.WriteFile(filepath.Join(out, "markdown.css"), []byte(mdCSS), 0o644); err != nil {
+		return err
+	}
+
+	// chroma.css for syntax highlighting and diff.css for diff highlighting
+	style := styles.Get(params.Style)
+	if style != nil {
+		formatter := html.New(
+			html.WithClasses(true),
+			html.WithCSSComments(false),
+		)
+		var cssBuf strings.Builder
+		if err := formatter.WriteCSS(&cssBuf, style); err != nil {
+			return err
+		}
+		if err := os.WriteFile(filepath.Join(out, "chroma.css"), []byte(cssBuf.String()), 0o644); err != nil {
+			return err
+		}
+
+		diffFormatter := html.New(
+			html.WithClasses(true),
+			html.WithCSSComments(false),
+			html.WithCustomCSS(map[chroma.TokenType]string{
+				chroma.GenericInserted: "display: block;",
+				chroma.GenericDeleted:  "display: block;",
+			}),
+		)
+		var diffBuf strings.Builder
+		if err := diffFormatter.WriteCSS(&diffBuf, style); err != nil {
+			return err
+		}
+		if err := os.WriteFile(filepath.Join(out, "diff.css"), []byte(diffBuf.String()), 0o644); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
tags.go
@@ -31,6 +31,7 @@ func generateTags(entries []git.Tag, params Params) error {
 			RootHref:      rootHref,
 			CurrentRefDir: params.DefaultRef.DirName(),
 			Selected:      "tags",
+			InlineStyles:  params.InlineStyles,
 		},
 		Tags: entries,
 	})