OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      3beadbaa9293
DATE      2026-06-15
SUBJECT   forgejo: custom colorscheme
FILES     4 CHANGED
HASH      3beadbaa9293
DATE      2026-06-15
SUBJECT   forgejo: custom colorscheme
FILES     4 CHANGED
 

diff --git a/hosts/linode/default.nix b/hosts/linode/default.nix
index b89bfdf..aaa8981 100644
--- a/hosts/linode/default.nix
+++ b/hosts/linode/default.nix
@@ -4,5 +4,10 @@
     ./base
   ];
 
+  ooknet.server.forgejo = {
+    customTheme.enable = true;
+    hideLogo = true;
+  };
+
   system.stateVersion = "24.11";
 }

diff --git a/hosts/linode/default.nix b/host
s/linode/default.nix
index b89bfdf..aaa8981 100644
--- a/hosts/linode/default.nix
+++ b/hosts/linode/default.nix
@@ -4,5 +4,10 @@
     ./base
   ];
 
+  ooknet.server.forgejo = {
+    customTheme.enable = true;
+    hideLogo = true;
+  };
+
   system.stateVersion = "24.11";
 }
 

diff --git a/modules/nixos/server/services/forgejo/default.nix b/modules/nixos/ser
ver/services/forgejo/default.nix
index 4c6ac05..fffffb3 100644
--- a/modules/nixos/server/services/forgejo/default.nix
+++ b/modules/nixos/server/services/forgejo/default.nix
@@ -2,20 +2,53 @@
   config,
   lib,
   pkgs,
+  ook,
   ...
 }: let
-  inherit (lib) mkIf elem getExe;
+  inherit (lib) mkIf elem getExe optionalAttrs optionals;
 
   inherit (config.ooknet.server) services domain;
   inherit (config.services) fail2ban;
 
+  forgejoCfg = config.ooknet.server.forgejo;
+  themeCfg = forgejoCfg.customTheme;
+  themeFile = pkgs.writeText "theme-${themeCfg.name}.css" (
+    ook.lib.color.export.toForgejoTheme {scheme = ook.themes.dark;}
+  );
+
+  blankLogoSvg = pkgs.writeText "logo.svg" ''<svg xmlns="http://www.w3.org/2000/s
vg" viewBox="0 0 1 1"></svg>'';
+  blankLogoPng = pkgs.runCommand "logo.png" {nativeBuildInputs = [pkgs.imagemagic
k];} ''
+    magick -size 1x1 xc:transparent PNG32:$out
+  '';
+
   settingsFormat = lib.generators.toINI {
     mkKeyValue = lib.generators.mkKeyValueDefault {} " = ";
   };
 in {
+  imports = [./options.nix];
+
   config = mkIf (elem "forgejo" services) {
     networking.firewall.allowedTCPPorts = [2222];
 
+    systemd.tmpfiles.rules = let
+      fj = config.services.forgejo;
+      pub = "${fj.customDir}/public";
+      mkdir = p: "d ${p} 0750 ${fj.user} ${fj.group} - -";
+    in
+      optionals (themeCfg.enable || forgejoCfg.hideLogo) [
+        (mkdir pub)
+        (mkdir "${pub}/assets")
+      ]
+      ++ optionals themeCfg.enable [
+        (mkdir "${pub}/assets/css")
+        "L+ ${pub}/assets/css/theme-${themeCfg.name}.css - - - - ${themeFile}"
+      ]
+      ++ optionals forgejoCfg.hideLogo [
+        (mkdir "${pub}/assets/img")
+        "L+ ${pub}/assets/img/logo.svg - - - - ${blankLogoSvg}"
+        "L+ ${pub}/assets/img/logo.png - - - - ${blankLogoPng}"
+      ];
+
     ooknet.server = {
       webserver.caddy.enable = true;
       database.postgresql.enable = true;
@@ -45,6 +78,12 @@ in {
           security = {
             INSTALL_LOCK = true;
           };
+          ui = mkIf themeCfg.enable ({
+              THEMES = "forgejo-auto,forgejo-light,forgejo-dark,${themeCfg.name}"
;
+            }
+            // optionalAttrs themeCfg.default {
+              DEFAULT_THEME = themeCfg.name;
+            });
         };
       };
       caddy.virtualHosts = {

diff --git a/modules/nixos/server/services/f
orgejo/default.nix b/modules/nixos/server/se
rvices/forgejo/default.nix
index 4c6ac05..fffffb3 100644
--- a/modules/nixos/server/services/forgejo/
default.nix
+++ b/modules/nixos/server/services/forgejo/
default.nix
@@ -2,20 +2,53 @@
   config,
   lib,
   pkgs,
+  ook,
   ...
 }: let
-  inherit (lib) mkIf elem getExe;
+  inherit (lib) mkIf elem getExe optionalAt
trs optionals;
 
   inherit (config.ooknet.server) services d
omain;
   inherit (config.services) fail2ban;
 
+  forgejoCfg = config.ooknet.server.forgejo
;
+  themeCfg = forgejoCfg.customTheme;
+  themeFile = pkgs.writeText "theme-${theme
Cfg.name}.css" (
+    ook.lib.color.export.toForgejoTheme {sc
heme = ook.themes.dark;}
+  );
+
+  blankLogoSvg = pkgs.writeText "logo.svg" 
''<svg xmlns="http://www.w3.org/2000/svg" vi
ewBox="0 0 1 1"></svg>'';
+  blankLogoPng = pkgs.runCommand "logo.png"
 {nativeBuildInputs = [pkgs.imagemagick];} '
'
+    magick -size 1x1 xc:transparent PNG32:$
out
+  '';
+
   settingsFormat = lib.generators.toINI {
     mkKeyValue = lib.generators.mkKeyValueD
efault {} " = ";
   };
 in {
+  imports = [./options.nix];
+
   config = mkIf (elem "forgejo" services) {
     networking.firewall.allowedTCPPorts = [
2222];
 
+    systemd.tmpfiles.rules = let
+      fj = config.services.forgejo;
+      pub = "${fj.customDir}/public";
+      mkdir = p: "d ${p} 0750 ${fj.user} ${
fj.group} - -";
+    in
+      optionals (themeCfg.enable || forgejo
Cfg.hideLogo) [
+        (mkdir pub)
+        (mkdir "${pub}/assets")
+      ]
+      ++ optionals themeCfg.enable [
+        (mkdir "${pub}/assets/css")
+        "L+ ${pub}/assets/css/theme-${theme
Cfg.name}.css - - - - ${themeFile}"
+      ]
+      ++ optionals forgejoCfg.hideLogo [
+        (mkdir "${pub}/assets/img")
+        "L+ ${pub}/assets/img/logo.svg - - 
- - ${blankLogoSvg}"
+        "L+ ${pub}/assets/img/logo.png - - 
- - ${blankLogoPng}"
+      ];
+
     ooknet.server = {
       webserver.caddy.enable = true;
       database.postgresql.enable = true;
@@ -45,6 +78,12 @@ in {
           security = {
             INSTALL_LOCK = true;
           };
+          ui = mkIf themeCfg.enable ({
+              THEMES = "forgejo-auto,forgej
o-light,forgejo-dark,${themeCfg.name}";
+            }
+            // optionalAttrs themeCfg.defau
lt {
+              DEFAULT_THEME = themeCfg.name
;
+            });
         };
       };
       caddy.virtualHosts = {
 

diff --git a/modules/nixos/server/services/forgejo/options.nix b/modules/nixos/ser
ver/services/forgejo/options.nix
index e17cd82..f9e048e 100644
--- a/modules/nixos/server/services/forgejo/options.nix
+++ b/modules/nixos/server/services/forgejo/options.nix
@@ -3,18 +3,28 @@
   ...
 }: let
   inherit (lib) mkEnableOption mkOption;
-  inherit (lib.types) str;
-  mkColorOption = description:
-    mkOption {
-      type = str;
-      default = "";
-      inherit description;
-    };
+  inherit (lib.types) str bool;
 in {
-  options.ooknet.server.forgejo.customTheme = {
-    enable = mkEnableOption;
-    light = {
-      primary = mkColorOption;
+  options.ooknet.server.forgejo = {
+    # blanks the logo asset (custom/public/assets/img/logo.{svg,png}); the
+    # supported way to customize forgejo branding, no app.ini flag exists
+    hideLogo = mkOption {
+      type = bool;
+      default = false;
+      description = "Replace the Forgejo logo with a blank asset.";
+    };
+    customTheme = {
+      enable = mkEnableOption "ooknet-generated Forgejo color theme (from ook.the
mes.dark)";
+      name = mkOption {
+        type = str;
+        default = "ooknet";
+        description = "Theme name, served as theme-<name>.css and selectable in t
he UI.";
+      };
+      default = mkOption {
+        type = bool;
+        default = true;
+        description = "Set this theme as the instance DEFAULT_THEME.";
+      };
     };
   };
 }

diff --git a/modules/nixos/server/services/f
orgejo/options.nix b/modules/nixos/server/se
rvices/forgejo/options.nix
index e17cd82..f9e048e 100644
--- a/modules/nixos/server/services/forgejo/
options.nix
+++ b/modules/nixos/server/services/forgejo/
options.nix
@@ -3,18 +3,28 @@
   ...
 }: let
   inherit (lib) mkEnableOption mkOption;
-  inherit (lib.types) str;
-  mkColorOption = description:
-    mkOption {
-      type = str;
-      default = "";
-      inherit description;
-    };
+  inherit (lib.types) str bool;
 in {
-  options.ooknet.server.forgejo.customTheme
 = {
-    enable = mkEnableOption;
-    light = {
-      primary = mkColorOption;
+  options.ooknet.server.forgejo = {
+    # blanks the logo asset (custom/public/
assets/img/logo.{svg,png}); the
+    # supported way to customize forgejo br
anding, no app.ini flag exists
+    hideLogo = mkOption {
+      type = bool;
+      default = false;
+      description = "Replace the Forgejo lo
go with a blank asset.";
+    };
+    customTheme = {
+      enable = mkEnableOption "ooknet-gener
ated Forgejo color theme (from ook.themes.da
rk)";
+      name = mkOption {
+        type = str;
+        default = "ooknet";
+        description = "Theme name, served a
s theme-<name>.css and selectable in the UI.
";
+      };
+      default = mkOption {
+        type = bool;
+        default = true;
+        description = "Set this theme as th
e instance DEFAULT_THEME.";
+      };
     };
   };
 }
 

diff --git a/outputs/ook/lib/color/export.nix b/outputs/ook/lib/color/export.nix
index 88c04af..445b2d9 100644
--- a/outputs/ook/lib/color/export.nix
+++ b/outputs/ook/lib/color/export.nix
@@ -1,5 +1,5 @@
 {lib}: let
-  inherit (lib) concatStringsSep mapAttrsToList flatten;
+  inherit (lib) concatStringsSep mapAttrsToList flatten concatMap;
 
   # Convert a flat color to CSS custom property
   colorToCss = name: value: "  --ooknet-${name}: #${value};";
@@ -21,7 +21,11 @@
   ];
 
   # Convert semantic colors to CSS custom properties
-  semanticToCss = {layout, border, typography}: let
+  semanticToCss = {
+    layout,
+    border,
+    typography,
+  }: let
     layoutVars = mapAttrsToList (key: color: colorToCss "layout-${key}" color) la
yout;
     borderVars = mapAttrsToList (key: color: colorToCss "border-${key}" color) bo
rder;
     typographyVars = mapAttrsToList (key: color: colorToCss "typography-${key}" c
olor) typography;
@@ -40,49 +44,61 @@
     ];
 
     # Neutrals section
-    neutralsSection = [
-      ""
-      "  /* Neutrals */"
-    ] ++ (neutralsToCss scheme.neutrals);
+    neutralsSection =
+      [
+        ""
+        "  /* Neutrals */"
+      ]
+      ++ (neutralsToCss scheme.neutrals);
 
     # Named colors section
     namedColors = ["red" "orange" "yellow" "olive" "green" "teal" "blue" "violet"
 "purple" "pink" "brown" "primary"];
-    namedColorsSection = [
-      ""
-      "  /* Named colors */"
-    ] ++ (flatten (map (color: scaleToCss color scheme.${color}) namedColors));
+    namedColorsSection =
+      [
+        ""
+        "  /* Named colors */"
+      ]
+      ++ (flatten (map (color: scaleToCss color scheme.${color}) namedColors));
 
     # Secondary section
-    secondarySection = [
-      ""
-      "  /* Secondary */"
-    ] ++ (scaleToCss "secondary" scheme.secondary);
+    secondarySection =
+      [
+        ""
+        "  /* Secondary */"
+      ]
+      ++ (scaleToCss "secondary" scheme.secondary);
 
     # Alert section
-    alertSection = [
-      ""
-      "  /* Alerts */"
-    ] ++ (flatten [
-      (alertToCss "error" scheme.error)
-      (alertToCss "warning" scheme.warning)
-      (alertToCss "success" scheme.success)
-      (alertToCss "note" scheme.note)
-      (alertToCss "tip" scheme.tip)
-    ]);
+    alertSection =
+      [
+        ""
+        "  /* Alerts */"
+      ]
+      ++ (flatten [
+        (alertToCss "error" scheme.error)
+        (alertToCss "warning" scheme.warning)
+        (alertToCss "success" scheme.success)
+        (alertToCss "note" scheme.note)
+        (alertToCss "tip" scheme.tip)
+      ]);
 
     # Semantic section
-    semanticSection = [
-      ""
-      "  /* Semantic */"
-    ] ++ (semanticToCss {
-      inherit (scheme) layout border typography;
-    });
+    semanticSection =
+      [
+        ""
+        "  /* Semantic */"
+      ]
+      ++ (semanticToCss {
+        inherit (scheme) layout border typography;
+      });
 
     # Syntax section
-    syntaxSection = [
-      ""
-      "  /* Syntax */"
-    ] ++ (syntaxToCss scheme.syntax);
+    syntaxSection =
+      [
+        ""
+        "  /* Syntax */"
+      ]
+      ++ (syntaxToCss scheme.syntax);
 
     # Base16 section
     base16Section = [
@@ -131,6 +147,249 @@
     ];
   in
     concatStringsSep "\n" allSections;
+
+  # TODO: move to ooknet-style
+  # generate a full Forgejo theme from a scheme. imports a built-in base
+  # for the bits we dont recolor (chroma syntax, codemirror, markup) then
+  # overrides the whole UI palette. nearly everything in forgejos theme
+  # references --steel-* so redefining those cascades to body/box/text/etc
+  toForgejoTheme = {
+    scheme,
+    base ? "forgejo-dark",
+  }: let
+    inherit (scheme) neutrals primary;
+    v = name: hex: "  --${name}: #${hex};";
+    ref = name: target: "  --${name}: var(--${target});";
+
+    # forgejos neutral ramp (900 darkest -> 100 lightest), 1:1 with ook neutrals
+    steelKeys = ["900" "850" "800" "750" "700" "650" "600" "550" "500" "450" "400
" "350" "300" "250" "200" "150" "100"];
+    steel = map (k: v "steel-${k}" neutrals.${k}) steelKeys;
+
+    # dark theme: primary-dark-N is lighter (hard), primary-light-N is darker (so
ft)
+    alphas = [
+      {
+        p = "10";
+        h = "19";
+      }
+      {
+        p = "20";
+        h = "33";
+      }
+      {
+        p = "30";
+        h = "4b";
+      }
+      {
+        p = "40";
+        h = "66";
+      }
+      {
+        p = "50";
+        h = "80";
+      }
+      {
+        p = "60";
+        h = "99";
+      }
+      {
+        p = "70";
+        h = "b3";
+      }
+      {
+        p = "80";
+        h = "cc";
+      }
+      {
+        p = "90";
+        h = "e1";
+      }
+    ];
+    primaryVars =
+      [
+        (v "color-primary" primary.base)
+        (ref "color-primary-contrast" "steel-900")
+        (v "color-primary-dark-1" primary.hard1)
+        (v "color-primary-dark-2" primary.hard1)
+        (v "color-primary-dark-3" primary.hard2)
+        (v "color-primary-dark-4" primary.hard2)
+        (v "color-primary-dark-5" primary.hard3)
+        (v "color-primary-dark-6" primary.hard3)
+        (v "color-primary-dark-7" primary.hard4)
+        (v "color-primary-light-1" primary.soft1)
+        (v "color-primary-light-2" primary.soft2)
+        (v "color-primary-light-3" primary.soft3)
+        (v "color-primary-light-4" primary.soft4)
+        (v "color-primary-light-5" primary.soft4)
+        (v "color-primary-light-6" primary.soft4)
+        (v "color-primary-light-7" primary.soft4)
+        (ref "color-primary-hover" "color-primary-light-1")
+        (ref "color-primary-active" "color-primary-light-2")
+      ]
+      ++ (map (a: v "color-primary-alpha-${a.p}" "${primary.base}${a.h}") alphas)
;
+
+    named = ["red" "orange" "yellow" "olive" "green" "teal" "blue" "violet" "purp
le" "pink" "brown"];
+    namedVars =
+      (concatMap (n: let
+          s = scheme.${n};
+        in [
+          (v "color-${n}" s.base)
+          (v "color-${n}-light" s.hard1)
+          (v "color-${n}-dark-1" s.soft1)
+          (v "color-${n}-dark-2" s.soft2)
+        ])
+        named)
+      ++ [
+        (ref "color-grey" "steel-500")
+        (ref "color-grey-light" "steel-300")
+        (ref "color-black" "steel-900")
+        (ref "color-black-light" "steel-850")
+      ];
+
+    badgeVars = concatMap (n: let
+      b = scheme.${n}.base;
+    in [
+      (v "color-${n}-badge" b)
+      (v "color-${n}-badge-bg" "${b}22")
+      (v "color-${n}-badge-hover-bg" "${b}44")
+    ]) ["red" "green" "yellow" "orange"];
+
+    consoleVars = [
+      (ref "color-console-fg" "steel-100")
+      (ref "color-console-fg-subtle" "steel-300")
+      (ref "color-console-bg" "steel-850")
+      (ref "color-console-border" "steel-700")
+      "  --color-console-hover-bg: #ffffff16;"
+      (ref "color-console-active-bg" "steel-650")
+      (ref "color-console-menu-bg" "steel-700")
+      (ref "color-console-menu-border" "steel-550")
+      (ref "color-ansi-black" "steel-800")
+      (v "color-ansi-red" scheme.red.base)
+      (v "color-ansi-green" scheme.green.base)
+      (v "color-ansi-yellow" scheme.yellow.base)
+      (v "color-ansi-blue" scheme.blue.base)
+      (v "color-ansi-magenta" scheme.purple.base)
+      (v "color-ansi-cyan" scheme.teal.base)
+      (ref "color-ansi-white" "steel-200")
+      (ref "color-ansi-bright-black" "steel-500")
+      (v "color-ansi-bright-red" scheme.red.hard1)
+      (v "color-ansi-bright-green" scheme.green.hard1)
+      (v "color-ansi-bright-yellow" scheme.yellow.hard1)
+      (v "color-ansi-bright-blue" scheme.blue.hard1)
+      (v "color-ansi-bright-magenta" scheme.purple.hard1)
+      (v "color-ansi-bright-cyan" scheme.teal.hard1)
+      (ref "color-ansi-bright-white" "steel-100")
+    ];
+
+    # syntax highlighting (chroma). forgejo ships these as hardcoded
+    # `.chroma .X { color }` rules; we re-emit them from the semantic
+    # syntax palette (gruvbox-material assignments). class -> token type
+    # per https://github.com/alecthomas/chroma types.go
+    syn = scheme.syntax;
+    hx = h: "#${h}";
+    text = "var(--color-text)";
+    crule = class: color: "  .chroma .${class} { color: ${color}; }";
+    chromaRules = [
+      (crule "k" (hx syn.statement)) # keyword
+      (crule "kc" (hx syn.boolean)) # keyword constant
+      (crule "kd" (hx syn.storageClass)) # keyword declaration
+      (crule "kn" (hx syn.include)) # keyword namespace
+      (crule "kp" (hx syn.statement)) # keyword pseudo
+      (crule "kr" (hx syn.statement)) # keyword reserved
+      (crule "kt" (hx syn.type)) # keyword type
+      (crule "n" text) # name
+      (crule "na" (hx syn.identifier)) # name attribute
+      (crule "nb" (hx syn.function)) # name builtin
+      (crule "bp" (hx syn.function)) # name builtin pseudo
+      (crule "nc" (hx syn.type)) # name class
+      (crule "nd" (hx syn.macro)) # name decorator
+      (crule "ne" (hx syn.exception)) # name exception
+      (crule "nf" (hx syn.function)) # name function
+      (crule "fm" (hx syn.function)) # name function magic
+      (crule "ni" (hx syn.special)) # name entity
+      (crule "nl" (hx syn.label)) # name label
+      (crule "nn" text) # name namespace
+      (crule "no" (hx syn.constant)) # name constant
+      (crule "nt" (hx syn.tag)) # name tag
+      (crule "nv" (hx syn.identifier)) # name variable
+      (crule "vc" (hx syn.identifier)) # name variable class
+      (crule "vg" (hx syn.identifier)) # name variable global
+      (crule "vi" (hx syn.identifier)) # name variable instance
+      (crule "nx" text) # name other
+      (crule "s" (hx syn.string)) # string
+      (crule "s1" (hx syn.string)) # string single
+      (crule "s2" (hx syn.string)) # string double
+      (crule "sb" (hx syn.string)) # string backtick
+      (crule "sc" (hx syn.string)) # string char
+      (crule "sd" (hx syn.string)) # string doc
+      (crule "sh" (hx syn.string)) # string heredoc
+      (crule "sx" (hx syn.string)) # string other
+      (crule "sa" (hx syn.storageClass)) # string affix
+      (crule "dl" (hx syn.string)) # string delimiter
+      (crule "se" (hx syn.specialChar)) # string escape
+      (crule "ss" (hx syn.specialChar)) # string symbol
+      (crule "si" (hx syn.special)) # string interpol
+      (crule "sr" (hx syn.special)) # string regex
+      (crule "m" (hx syn.number)) # number
+      (crule "mb" (hx syn.number)) # number bin
+      (crule "mf" (hx syn.number)) # number float
+      (crule "mh" (hx syn.number)) # number hex
+      (crule "mi" (hx syn.number)) # number int
+      (crule "mo" (hx syn.number)) # number oct
+      (crule "il" (hx syn.number)) # number int long
+      (crule "o" (hx syn.operator)) # operator
+      (crule "ow" (hx syn.statement)) # operator word
+      (crule "p" text) # punctuation
+      (crule "c" (hx syn.comment)) # comment
+      (crule "c1" (hx syn.comment)) # comment single
+      (crule "ch" (hx syn.comment)) # comment hashbang
+      (crule "cm" (hx syn.comment)) # comment multiline
+      (crule "cs" (hx syn.comment)) # comment special
+      (crule "cp" (hx syn.preproc)) # comment preproc
+      (crule "cpf" (hx syn.preproc)) # comment preproc file
+      (crule "go" (hx syn.comment)) # generic output
+      (crule "gp" (hx syn.comment)) # generic prompt
+      (crule "gh" (hx syn.identifier)) # generic heading
+      (crule "gu" (hx syn.identifier)) # generic subheading
+      (crule "ge" (hx syn.identifier)) # generic emph
+      (crule "gs" text) # generic strong
+      (crule "gr" (hx syn.exception)) # generic error
+      (crule "gt" (hx syn.exception)) # generic traceback
+      (crule "gd" (hx syn.exception)) # generic deleted (bg kept from base)
+      (crule "gi" (hx syn.function)) # generic inserted (bg kept from base)
+      (crule "w" (hx syn.comment)) # whitespace
+    ];
+
+    lines = flatten [
+      "/* ooknet generated forgejo theme: ${scheme.slug} */"
+      "@import url(\"theme-${base}.css\");"
+      ""
+      ":root {"
+      "  --is-dark-theme: true;"
+      ""
+      "  /* neutrals */"
+      steel
+      ""
+      "  /* primary */"
+      primaryVars
+      ""
+      "  /* named colors */"
+      namedVars
+      ""
+      "  /* badges */"
+      badgeVars
+      ""
+      "  /* console + ansi */"
+      consoleVars
+      ""
+      "  color-scheme: dark;"
+      "  accent-color: var(--color-accent);"
+      "}"
+      ""
+      "/* syntax highlighting (chroma) */"
+      chromaRules
+    ];
+  in
+    concatStringsSep "\n" lines;
 in {
-  inherit toScss;
+  inherit toScss toForgejoTheme;
 }

diff --git a/outputs/ook/lib/color/export.ni
x b/outputs/ook/lib/color/export.nix
index 88c04af..445b2d9 100644
--- a/outputs/ook/lib/color/export.nix
+++ b/outputs/ook/lib/color/export.nix
@@ -1,5 +1,5 @@
 {lib}: let
-  inherit (lib) concatStringsSep mapAttrsTo
List flatten;
+  inherit (lib) concatStringsSep mapAttrsTo
List flatten concatMap;
 
   # Convert a flat color to CSS custom prop
erty
   colorToCss = name: value: "  --ooknet-${n
ame}: #${value};";
@@ -21,7 +21,11 @@
   ];
 
   # Convert semantic colors to CSS custom p
roperties
-  semanticToCss = {layout, border, typograp
hy}: let
+  semanticToCss = {
+    layout,
+    border,
+    typography,
+  }: let
     layoutVars = mapAttrsToList (key: color
: colorToCss "layout-${key}" color) layout;
     borderVars = mapAttrsToList (key: color
: colorToCss "border-${key}" color) border;
     typographyVars = mapAttrsToList (key: c
olor: colorToCss "typography-${key}" color) 
typography;
@@ -40,49 +44,61 @@
     ];
 
     # Neutrals section
-    neutralsSection = [
-      ""
-      "  /* Neutrals */"
-    ] ++ (neutralsToCss scheme.neutrals);
+    neutralsSection =
+      [
+        ""
+        "  /* Neutrals */"
+      ]
+      ++ (neutralsToCss scheme.neutrals);
 
     # Named colors section
     namedColors = ["red" "orange" "yellow" 
"olive" "green" "teal" "blue" "violet" "purp
le" "pink" "brown" "primary"];
-    namedColorsSection = [
-      ""
-      "  /* Named colors */"
-    ] ++ (flatten (map (color: scaleToCss c
olor scheme.${color}) namedColors));
+    namedColorsSection =
+      [
+        ""
+        "  /* Named colors */"
+      ]
+      ++ (flatten (map (color: scaleToCss c
olor scheme.${color}) namedColors));
 
     # Secondary section
-    secondarySection = [
-      ""
-      "  /* Secondary */"
-    ] ++ (scaleToCss "secondary" scheme.sec
ondary);
+    secondarySection =
+      [
+        ""
+        "  /* Secondary */"
+      ]
+      ++ (scaleToCss "secondary" scheme.sec
ondary);
 
     # Alert section
-    alertSection = [
-      ""
-      "  /* Alerts */"
-    ] ++ (flatten [
-      (alertToCss "error" scheme.error)
-      (alertToCss "warning" scheme.warning)
-      (alertToCss "success" scheme.success)
-      (alertToCss "note" scheme.note)
-      (alertToCss "tip" scheme.tip)
-    ]);
+    alertSection =
+      [
+        ""
+        "  /* Alerts */"
+      ]
+      ++ (flatten [
+        (alertToCss "error" scheme.error)
+        (alertToCss "warning" scheme.warnin
g)
+        (alertToCss "success" scheme.succes
s)
+        (alertToCss "note" scheme.note)
+        (alertToCss "tip" scheme.tip)
+      ]);
 
     # Semantic section
-    semanticSection = [
-      ""
-      "  /* Semantic */"
-    ] ++ (semanticToCss {
-      inherit (scheme) layout border typogr
aphy;
-    });
+    semanticSection =
+      [
+        ""
+        "  /* Semantic */"
+      ]
+      ++ (semanticToCss {
+        inherit (scheme) layout border typo
graphy;
+      });
 
     # Syntax section
-    syntaxSection = [
-      ""
-      "  /* Syntax */"
-    ] ++ (syntaxToCss scheme.syntax);
+    syntaxSection =
+      [
+        ""
+        "  /* Syntax */"
+      ]
+      ++ (syntaxToCss scheme.syntax);
 
     # Base16 section
     base16Section = [
@@ -131,6 +147,249 @@
     ];
   in
     concatStringsSep "\n" allSections;
+
+  # TODO: move to ooknet-style
+  # generate a full Forgejo theme from a sc
heme. imports a built-in base
+  # for the bits we dont recolor (chroma sy
ntax, codemirror, markup) then
+  # overrides the whole UI palette. nearly 
everything in forgejos theme
+  # references --steel-* so redefining thos
e cascades to body/box/text/etc
+  toForgejoTheme = {
+    scheme,
+    base ? "forgejo-dark",
+  }: let
+    inherit (scheme) neutrals primary;
+    v = name: hex: "  --${name}: #${hex};";
+    ref = name: target: "  --${name}: var(-
-${target});";
+
+    # forgejos neutral ramp (900 darkest ->
 100 lightest), 1:1 with ook neutrals
+    steelKeys = ["900" "850" "800" "750" "7
00" "650" "600" "550" "500" "450" "400" "350
" "300" "250" "200" "150" "100"];
+    steel = map (k: v "steel-${k}" neutrals
.${k}) steelKeys;
+
+    # dark theme: primary-dark-N is lighter
 (hard), primary-light-N is darker (soft)
+    alphas = [
+      {
+        p = "10";
+        h = "19";
+      }
+      {
+        p = "20";
+        h = "33";
+      }
+      {
+        p = "30";
+        h = "4b";
+      }
+      {
+        p = "40";
+        h = "66";
+      }
+      {
+        p = "50";
+        h = "80";
+      }
+      {
+        p = "60";
+        h = "99";
+      }
+      {
+        p = "70";
+        h = "b3";
+      }
+      {
+        p = "80";
+        h = "cc";
+      }
+      {
+        p = "90";
+        h = "e1";
+      }
+    ];
+    primaryVars =
+      [
+        (v "color-primary" primary.base)
+        (ref "color-primary-contrast" "stee
l-900")
+        (v "color-primary-dark-1" primary.h
ard1)
+        (v "color-primary-dark-2" primary.h
ard1)
+        (v "color-primary-dark-3" primary.h
ard2)
+        (v "color-primary-dark-4" primary.h
ard2)
+        (v "color-primary-dark-5" primary.h
ard3)
+        (v "color-primary-dark-6" primary.h
ard3)
+        (v "color-primary-dark-7" primary.h
ard4)
+        (v "color-primary-light-1" primary.
soft1)
+        (v "color-primary-light-2" primary.
soft2)
+        (v "color-primary-light-3" primary.
soft3)
+        (v "color-primary-light-4" primary.
soft4)
+        (v "color-primary-light-5" primary.
soft4)
+        (v "color-primary-light-6" primary.
soft4)
+        (v "color-primary-light-7" primary.
soft4)
+        (ref "color-primary-hover" "color-p
rimary-light-1")
+        (ref "color-primary-active" "color-
primary-light-2")
+      ]
+      ++ (map (a: v "color-primary-alpha-${
a.p}" "${primary.base}${a.h}") alphas);
+
+    named = ["red" "orange" "yellow" "olive
" "green" "teal" "blue" "violet" "purple" "p
ink" "brown"];
+    namedVars =
+      (concatMap (n: let
+          s = scheme.${n};
+        in [
+          (v "color-${n}" s.base)
+          (v "color-${n}-light" s.hard1)
+          (v "color-${n}-dark-1" s.soft1)
+          (v "color-${n}-dark-2" s.soft2)
+        ])
+        named)
+      ++ [
+        (ref "color-grey" "steel-500")
+        (ref "color-grey-light" "steel-300"
)
+        (ref "color-black" "steel-900")
+        (ref "color-black-light" "steel-850
")
+      ];
+
+    badgeVars = concatMap (n: let
+      b = scheme.${n}.base;
+    in [
+      (v "color-${n}-badge" b)
+      (v "color-${n}-badge-bg" "${b}22")
+      (v "color-${n}-badge-hover-bg" "${b}4
4")
+    ]) ["red" "green" "yellow" "orange"];
+
+    consoleVars = [
+      (ref "color-console-fg" "steel-100")
+      (ref "color-console-fg-subtle" "steel
-300")
+      (ref "color-console-bg" "steel-850")
+      (ref "color-console-border" "steel-70
0")
+      "  --color-console-hover-bg: #ffffff1
6;"
+      (ref "color-console-active-bg" "steel
-650")
+      (ref "color-console-menu-bg" "steel-7
00")
+      (ref "color-console-menu-border" "ste
el-550")
+      (ref "color-ansi-black" "steel-800")
+      (v "color-ansi-red" scheme.red.base)
+      (v "color-ansi-green" scheme.green.ba
se)
+      (v "color-ansi-yellow" scheme.yellow.
base)
+      (v "color-ansi-blue" scheme.blue.base
)
+      (v "color-ansi-magenta" scheme.purple
.base)
+      (v "color-ansi-cyan" scheme.teal.base
)
+      (ref "color-ansi-white" "steel-200")
+      (ref "color-ansi-bright-black" "steel
-500")
+      (v "color-ansi-bright-red" scheme.red
.hard1)
+      (v "color-ansi-bright-green" scheme.g
reen.hard1)
+      (v "color-ansi-bright-yellow" scheme.
yellow.hard1)
+      (v "color-ansi-bright-blue" scheme.bl
ue.hard1)
+      (v "color-ansi-bright-magenta" scheme
.purple.hard1)
+      (v "color-ansi-bright-cyan" scheme.te
al.hard1)
+      (ref "color-ansi-bright-white" "steel
-100")
+    ];
+
+    # syntax highlighting (chroma). forgejo
 ships these as hardcoded
+    # `.chroma .X { color }` rules; we re-e
mit them from the semantic
+    # syntax palette (gruvbox-material assi
gnments). class -> token type
+    # per https://github.com/alecthomas/chr
oma types.go
+    syn = scheme.syntax;
+    hx = h: "#${h}";
+    text = "var(--color-text)";
+    crule = class: color: "  .chroma .${cla
ss} { color: ${color}; }";
+    chromaRules = [
+      (crule "k" (hx syn.statement)) # keyw
ord
+      (crule "kc" (hx syn.boolean)) # keywo
rd constant
+      (crule "kd" (hx syn.storageClass)) # 
keyword declaration
+      (crule "kn" (hx syn.include)) # keywo
rd namespace
+      (crule "kp" (hx syn.statement)) # key
word pseudo
+      (crule "kr" (hx syn.statement)) # key
word reserved
+      (crule "kt" (hx syn.type)) # keyword 
type
+      (crule "n" text) # name
+      (crule "na" (hx syn.identifier)) # na
me attribute
+      (crule "nb" (hx syn.function)) # name
 builtin
+      (crule "bp" (hx syn.function)) # name
 builtin pseudo
+      (crule "nc" (hx syn.type)) # name cla
ss
+      (crule "nd" (hx syn.macro)) # name de
corator
+      (crule "ne" (hx syn.exception)) # nam
e exception
+      (crule "nf" (hx syn.function)) # name
 function
+      (crule "fm" (hx syn.function)) # name
 function magic
+      (crule "ni" (hx syn.special)) # name 
entity
+      (crule "nl" (hx syn.label)) # name la
bel
+      (crule "nn" text) # name namespace
+      (crule "no" (hx syn.constant)) # name
 constant
+      (crule "nt" (hx syn.tag)) # name tag
+      (crule "nv" (hx syn.identifier)) # na
me variable
+      (crule "vc" (hx syn.identifier)) # na
me variable class
+      (crule "vg" (hx syn.identifier)) # na
me variable global
+      (crule "vi" (hx syn.identifier)) # na
me variable instance
+      (crule "nx" text) # name other
+      (crule "s" (hx syn.string)) # string
+      (crule "s1" (hx syn.string)) # string
 single
+      (crule "s2" (hx syn.string)) # string
 double
+      (crule "sb" (hx syn.string)) # string
 backtick
+      (crule "sc" (hx syn.string)) # string
 char
+      (crule "sd" (hx syn.string)) # string
 doc
+      (crule "sh" (hx syn.string)) # string
 heredoc
+      (crule "sx" (hx syn.string)) # string
 other
+      (crule "sa" (hx syn.storageClass)) # 
string affix
+      (crule "dl" (hx syn.string)) # string
 delimiter
+      (crule "se" (hx syn.specialChar)) # s
tring escape
+      (crule "ss" (hx syn.specialChar)) # s
tring symbol
+      (crule "si" (hx syn.special)) # strin
g interpol
+      (crule "sr" (hx syn.special)) # strin
g regex
+      (crule "m" (hx syn.number)) # number
+      (crule "mb" (hx syn.number)) # number
 bin
+      (crule "mf" (hx syn.number)) # number
 float
+      (crule "mh" (hx syn.number)) # number
 hex
+      (crule "mi" (hx syn.number)) # number
 int
+      (crule "mo" (hx syn.number)) # number
 oct
+      (crule "il" (hx syn.number)) # number
 int long
+      (crule "o" (hx syn.operator)) # opera
tor
+      (crule "ow" (hx syn.statement)) # ope
rator word
+      (crule "p" text) # punctuation
+      (crule "c" (hx syn.comment)) # commen
t
+      (crule "c1" (hx syn.comment)) # comme
nt single
+      (crule "ch" (hx syn.comment)) # comme
nt hashbang
+      (crule "cm" (hx syn.comment)) # comme
nt multiline
+      (crule "cs" (hx syn.comment)) # comme
nt special
+      (crule "cp" (hx syn.preproc)) # comme
nt preproc
+      (crule "cpf" (hx syn.preproc)) # comm
ent preproc file
+      (crule "go" (hx syn.comment)) # gener
ic output
+      (crule "gp" (hx syn.comment)) # gener
ic prompt
+      (crule "gh" (hx syn.identifier)) # ge
neric heading
+      (crule "gu" (hx syn.identifier)) # ge
neric subheading
+      (crule "ge" (hx syn.identifier)) # ge
neric emph
+      (crule "gs" text) # generic strong
+      (crule "gr" (hx syn.exception)) # gen
eric error
+      (crule "gt" (hx syn.exception)) # gen
eric traceback
+      (crule "gd" (hx syn.exception)) # gen
eric deleted (bg kept from base)
+      (crule "gi" (hx syn.function)) # gene
ric inserted (bg kept from base)
+      (crule "w" (hx syn.comment)) # whites
pace
+    ];
+
+    lines = flatten [
+      "/* ooknet generated forgejo theme: $
{scheme.slug} */"
+      "@import url(\"theme-${base}.css\");"
+      ""
+      ":root {"
+      "  --is-dark-theme: true;"
+      ""
+      "  /* neutrals */"
+      steel
+      ""
+      "  /* primary */"
+      primaryVars
+      ""
+      "  /* named colors */"
+      namedVars
+      ""
+      "  /* badges */"
+      badgeVars
+      ""
+      "  /* console + ansi */"
+      consoleVars
+      ""
+      "  color-scheme: dark;"
+      "  accent-color: var(--color-accent);
"
+      "}"
+      ""
+      "/* syntax highlighting (chroma) */"
+      chromaRules
+    ];
+  in
+    concatStringsSep "\n" lines;
 in {
-  inherit toScss;
+  inherit toScss toForgejoTheme;
 }
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET