CSS and Assets

CSS entry file

Braised processes a single CSS entry file through the Tailwind CLI and writes the result to dist/assets/braised.css. The entry file is resolved in priority order:

  1. css.input in braised.yaml — explicit path, relative to project root (deprecated; use the theme layer instead)
  2. theme/main.css at the project root — site-level CSS override
  3. {themeDir}/theme/main.css — theme-supplied CSS

If none of these exist, the CSS pipeline is skipped and no braised.css is written. Your layout must supply its own stylesheet in that case.

Built-in CSS imports

The braised binary embeds two CSS files that any entry file can pull in:

@import "braised:theme/base";    /* layout, typography, color tokens */
@import "braised:theme/syntax";  /* syntax highlighting for code blocks */

These are not filesystem files — braised inlines them before handing the entry file to Tailwind. You can import them in a theme's theme/main.css or in a site-level override:

@import "braised:theme/base";
@import "braised:theme/syntax";

/* your additions follow */

Customization tiers

Tier 1 — Override CSS variables

The built-in theme is built on CSS custom properties. Override any of them after the import and the change propagates everywhere:

@import "braised:theme/base";

[data-theme="dark"] {
  --bg-base:      #1a1a2e;
  --accent:       #e94560;
  --text-primary: #eaeaea;
}

[data-theme="light"] {
  --bg-base:      #ffffff;
  --accent:       #c0392b;
  --text-primary: #1a1a2e;
}

See CSS variables reference for the full list.

Tier 2 — Add rules

@import "braised:theme/base";

.my-badge {
  display: inline-block;
  background: var(--accent-subtle);
  color: var(--accent-text);
  border-radius: 4px;
  padding: 0.1em 0.5em;
  font-size: 0.75em;
}

Tier 3 — Full replacement

Skip the braised imports entirely and provide your own styles. Braised compiles whatever is in the entry file — it does not require the built-in tokens.

/* no @import "braised:theme/base" */

:root { --brand: #0055cc; }
body  { font-family: system-ui; background: #fff; color: #111; }

Static assets

Braised has two copy paths for files that should be served verbatim.

Site static directory

{siteDir}/static/ is copied to the output root after every build. The directory structure is preserved exactly, so the output URL mirrors the source path:

static/
  favicon.ico          →  dist/favicon.ico           →  /favicon.ico
  robots.txt           →  dist/robots.txt             →  /robots.txt
  assets/
    logo.svg           →  dist/assets/logo.svg        →  /assets/logo.svg
  scripts/
    search-worker.js   →  dist/scripts/search-worker.js  →  /scripts/search-worker.js

Use static/ for anything that belongs to the site but not to any specific theme — favicons, robots.txt, custom JS workers, og-images, and so on.

Theme asset directories

A theme has two copy directories with different destinations:

Source Destination URL
{themeDir}/assets/ dist/assets/ /assets/…
{themeDir}/static/ dist/ (output root) /…

assets/ is specifically for theme-level files that live alongside braised's generated bundle. A file named blocks.js here replaces braised's generated bundle entirely:

themes/default/
  assets/
    blocks.js    ← replaces braised's generated bundle if present
    logo.svg
    font.woff2
  static/
    robots.txt   ← copied to dist/ root

Copy order and precedence

Files are written in this order, with later writes winning on collisions:

  1. Braised JS bundle → dist/assets/blocks.js
  2. Theme assets/dist/assets/ ← theme blocks.js replaces the generated bundle here
  3. Theme static/dist/
  4. Site static/dist/

Site-root static/ always wins over theme static/ on any filename collision.

Theme toggle

The built-in layout provides a two-state dark/light toggle. When you replace the layout, call:

document.documentElement.dataset.theme = 'light'; // or 'dark'

from your own JavaScript to drive the [data-theme] attribute. The initial theme is set by a small inline script — include this pattern in your layout's <head> to preserve the no-flash behavior:

<script>
  (function() {
    var stored = localStorage.getItem('braised-theme');
    var preferred = window.matchMedia('(prefers-color-scheme: light)').matches
      ? 'light' : 'dark';
    document.documentElement.dataset.theme = stored || preferred;
  })();
</script>

Config-driven themes

When themes: is set in braised.yaml, .Site.Themes.Values and .Site.Themes.Default are available in layout templates. Use them to render a theme switcher dynamically and apply the configured default on cold load:

<script>
  (function() {
    var stored = localStorage.getItem('braised-theme');
    {{if .Site.Themes.Default -}}
    var preferred = '{{.Site.Themes.Default}}';
    {{- else -}}
    var preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
    {{- end}}
    document.documentElement.dataset.theme = stored || preferred;
  })();
</script>

Render the switcher options from the config list:

{{range .Site.Themes.Values}}
<button class="theme-opt" data-theme="{{.}}">{{.}}</button>
{{end}}

When themes: is absent, both fields are empty and the fallback branch runs — existing layouts are unaffected.

The braised base CSS scopes all sidebar rules to .page-layout .sidebar. This means custom layouts that happen to use a .sidebar class are unaffected by braised's off-canvas behaviour.

The built-in breakpoints are tuned for the 3-column docs layout:

Breakpoint Behaviour
≤ 1100px Sidebar goes off-canvas (hamburger appears). TOC stays visible.
≤ 768px TOC collapses inline above content.

For a 2-column layout (sidebar + main, no TOC), 1100px is too aggressive — a 220px sidebar alongside content is comfortable down to 768px. Use your own selector and breakpoint:

/* 2-column layout — sidebar stays sticky down to tablet width */
.page-shell .sidebar {
  position: sticky;
  top: var(--header-height);
  height: calc(100vh - var(--header-height));
}

@media (max-width: 768px) {
  .nav-toggle { display: flex; }

  .page-shell {
    grid-template-columns: 1fr;
  }

  .page-shell .sidebar {
    position: fixed;
    top: var(--header-height);
    left: 0;
    width: var(--sidebar-width);
    height: calc(100vh - var(--header-height));
    transform: translateX(-100%);
    transition: transform 0.2s ease;
    z-index: 100;
  }
  .page-shell .sidebar.is-open { transform: translateX(0); }
}

The hamburger toggle JS (#nav-toggle, .sidebar, #nav-overlay) is provided by blocks.js automatically — include those three elements in your layout and the open/close behaviour works without any additional script.