Style, colour & themes

Every figure in the tour is dressed by the same cascade, so what this page shows about a chain of boxes holds for a chart, a sequence or a floor plan.

What the defaults already say

|box#ingest| "Ingest"
|box#queue| "Queue"
|cyl#store| "Store"

ingest -> queue "raw"
queue -> store "rows"
Ingest Queue Store raw rows

No stylesheet — and the figure still has a fill, a stroke colour, a corner radius, a font, a gap and a wire weight.

Those are the templates’ baked defaults: the bottom tier of the cascade, and what every section below is a way of overriding. A figure that says nothing about style is not unstyled.

One block at the root

{
  gap: 44; padding: 26;
  fill: --bg; color: --sky-ink; font-size: 13;
}

|box#ingest| "Ingest"
|box#queue| "Queue"
|cyl#store| "Store"

ingest -> queue "raw"
queue -> store "rows"
Ingest Queue Store raw rows

Declarations written straight into the root { } are the scene’s own properties. padding is the SVG margin, and fill: --bg paints the backdrop a figure otherwise leaves transparent.

The text family behaves differently from the rest: color, font-size, font-family and font-weight inherit down the tree, nearest ancestor winning. That is why both link labels take their colour and size from a root that never mentions them.

Captions and link labels size themselves as fractions of the inherited number, so one font-size at the root rescales the whole scene in proportion.

The ladder

{
  gap: 44;
  |box| { radius: 0; }
  .stage { fill: --amber-wash; stroke: --amber-deep; color: --amber-ink; }
  #store { stroke-width: 3; }
}

|box#ingest| "Ingest" .stage
|box#queue| "Queue" .stage
|cyl#store| "Store" .stage { fill: --gray-wash; }

ingest -> queue "raw"
queue -> store "rows"
Ingest Queue Store raw rows

A selector is a type (|box|, or |-| for links), a class, an id, or a space-separated run of those for descendants (|group| |box|).

They stack in five tiers, low to high:

  1. Type rules — |box| { }
  2. Descendant rules — |group| |box| { }
  3. Classes.stage { }
  4. The id rule — #store { }
  5. The node’s own { }

Most specific wins per property, and ties go to whichever was written later.

Follow the cylinder through all five and you can read the figure: it keeps its corners because |box| never matched it at all, takes its ink from .stage, its stroke weight from #store, and its grey from its own block.

Colour from the palette

{
  gap: 44;
  --brand: #6d5bd0;
  |svc::box| { fill: --teal-wash; stroke: --teal-deep; color: --teal-ink; }
  |-| { stroke: --gray-deep; }
  #store { fill: --brand; stroke: --brand; color: white; }
}

|svc#ingest| "Ingest"
|svc#queue| "Queue"
|svc#store| "Store"

ingest -> queue -> store
Ingest Queue Store

Eleven hues — red rose orange amber lime green teal sky blue purple gray — each carrying five tiers: wash, soft, the bare name, deep, ink.

They are named for the job they do, not for their lightness. Every colour is a light-dark() pair, so a pale surface in light mode has to become a dark one in dark mode — and a name like “light” would invert and start lying. --teal-wash is the faint surface either way.

Wash fill, deep stroke, ink text is the recipe worth reaching for; most figures in this tour are exactly that.

--brand is your own variable. --name covers colours and the font family, since sizes bake at compile time.

Gradients, hatches and shadows

{ gap: 30; }

|box#g| "gradient" { fill: gradient(--rose, --sky); color: white; stroke: none; }
|box#h| "hatch" { fill: hatch(45, 6, --gray-deep); }
|box#s| "shadow" { shadow: 3; }
|box#m| "multiple" { multiple: 3; fill: --lime-wash; stroke: --lime-deep; }
gradient hatch shadow multiple

Four ways to make a fill say more than a colour does:

  • gradient(a, b) blends two hues at a flattering angle, on a fill or a stroke. linear-gradient(angle, …) and radial-gradient(…) are the control gates when you want the angle yourself.
  • hatch(angle, pitch, colour) is the drafting section texture, and works on any fill.
  • shadow drops a soft shadow.
  • multiple draws offset duplicates behind a node — the “several of these” reading.

Dark mode and themes

{ gap: 40; fill: --bg; padding: 24; }

|box#a| "Ingest" { fill: --teal-wash; stroke: --teal-deep; color: --teal-ink; }
|box#b| "Store" { fill: --amber-wash; stroke: --amber-deep; color: --amber-ink; }

a -> b
Ingest Store

Toggle this book’s theme and the figure follows. Every colour Lini emits is a light-dark() pair keyed on color-scheme, so one SVG carries both palettes and follows the viewer’s OS with no script at all.

  • data-theme="dark" on any ancestor forces one.
  • At export, lini --theme dark pins a palette — light, high-contrast and blueprint too, or a CSS file of your own --lini-* overrides.
  • lini theme NAME prints one to start from.

Everything sits in @layer lini.defaults, so a host page re-themes a figure with plain CSS and no !important:

.lini { --lini-accent: #ff6600; --lini-font-family: Inter, sans-serif; }

Go deeper

Selectors, cascade & specificity has the full ladder, paint, stroke & text the property behaviour, and colour, variables & expressions the palette, gradients, hatches and the variable rules.