The CSS pipeline
The kit's two CSS entries, how a section's own CSS reaches the page, the cascade-layer order a published page loads in, where breakpoints come from, and why a frame is a viewport.
The kit owns only what is true of every template: the spacing base unit, the motion defaults, the reset gaps Tailwind’s preflight leaves, and the section box model. It ships no colour, font, type, radius or shadow of its own, and there is no shared file for you to edit to make a token of yours usable.
Two CSS entries ship, and they are not interchangeable.
| Entry | What it is | How it loads |
|---|---|---|
@homepages/template-kit/styles.css |
The global Tailwind preamble: --spacing, motion defaults, and @source registration of the kit’s compiled JS. No breakpoints — those are yours |
imported into your Tailwind entry, after Tailwind |
@homepages/template-kit/base.css |
Cascade-layer order, the three reset gaps Tailwind’s preflight leaves, .tr-section, .tr-frame, .tr-image-frame, and the motion tokens |
its own stylesheet on the page — not part of the Tailwind entry |
Your Tailwind entry
Section titled “Your Tailwind entry”/* your CSS entry */@import "tailwindcss";@import "@homepages/template-kit/styles.css";@import "./theme.css"; /* generated by `template-kit theme` from theme.ts */Three constraints, all of them silent when broken:
- Import Tailwind exactly once per build graph.
styles.cssdeliberately does not@import "tailwindcss"itself — yours does. Two double-emit Tailwind’s preflight reset (box-sizingdeclared twice, several KB of duplicate base layer), and nothing errors. - Your generated theme must be imported here, inside the Tailwind build graph. It
contains an
@themeblock, which is a Tailwind directive: anywhere outside the graph it is inert, and you get a stylesheet with no token utilities and no error explaining why. - Import it after
styles.css, so the kit’s globals are in place before your tokens land on top of them.
styles.css also carries an @source line registering the kit’s own shipped dist for
Tailwind’s class scan. That is how the utility classes baked into the kit’s primitives
(Image’s object-cover, …) reach your compiled CSS; it also overrides Tailwind’s default
exclusion of node_modules. You do not need to add a @source for the kit yourself.
Colour in sections
Section titled “Colour in sections”Why a section carries no palette of its own is the theme model.
In practice: the template-kit/no-hex lint rule flags a hard-coded hex
literal in a colour context, and a CSS colour function anywhere, without exempting
"use client" files the way most of the preset’s other rules do.
For a colour the theme does not name — a hover, active, or pressed step, or a translucent
tint — derive it from the tokens that are named, with a color-mix() over theme-derived
arguments:
hover:bg-[color-mix(in_srgb,var(--tr-color-primary)_80%,var(--tr-color-surface))]. Such a
mix composes a colour out of the theme rather than stating one, and is the single colour
function no-hex allows — it carves out no other.
How a section’s CSS reaches the page
Section titled “How a section’s CSS reaches the page”A section’s compiled CSS layer is assembled from two sources, both automatic — you never write an import to make either happen:
- Hand-written CSS, collected by path. Every
.cssfile inside the section’s folder —styles.cssand any other stylesheet nested under it — is picked up by walking the folder, wherever it sits, and concatenated into@layer sections. A template’s own hand-written stylesheet is collected the same way, into@layer template. - Package CSS, harvested from the render path. The build also bundles
Renderer.tsx’s module graph — every component and island it imports — and pulls in whatever.cssthose imports resolve to. A bare specifier (import "swiper/css";) resolves intonode_modulesand lands in the same@layer sections; that is the sanctioned way for a third-party package’s stylesheet to reach the page.
Two constraints follow directly, and both are enforced by lint:
- Don’t re-import your own stylesheet. It is already collected by path —
importing it again from a
.ts/.tsxfile only double-emits it, and can smuggle a stray@layerstatement into a nested layer. Seeno-css-import-from-render-path. @importnever works inside hand-written CSS, package or local. An@importlanding inside@layer sections/@layer templateis invalid CSS, and the browser drops it silently rather than erroring — the package’s styling is simply missing from the published page. Reach a package’s stylesheet from code instead. Seeno-bare-css-import.
The page’s stylesheets
Section titled “The page’s stylesheets”Why this order is fixed by the platform rather than negotiated per section is the theme model. A published page loads, in order:
base.css— layer order, the reset gaps,.tr-section,.tr-frame,.tr-image-frame, motion tokens.- Your compiled Tailwind bundle — your entry:
@import "tailwindcss"+ the kit’sstyles.css+ your compiled theme + the utilities your section markup uses.
The layer order base.css declares is:
@layer theme, base, components, sections, template, utilities;Utilities come last, so a Tailwind utility always beats the reset’s element defaults.
base.css requires a Tailwind entry
Section titled “base.css requires a Tailwind entry”base.css does not ship a reset. Your entry’s @import "tailwindcss" brings
Tailwind’s preflight, and preflight is the reset — box-sizing, margins, list-style, the
button and anchor resets. base.css carries only the three declarations preflight does
not make: font-smoothing, button { cursor: pointer } (Tailwind v4 leaves buttons at
cursor: default), and svg { max-width: 100% } (preflight caps only img and
video).
Ship base.css without a Tailwind entry on the page and you get an unreset page.
base.css references no design token of yours — nothing global is opinionated about
your brand. Its one tintable surface is the placeholder fill shared by the Image and
Video frames — one class, one --tr-image-frame-bg, so you retint both at once; see
Slot.Image/Slot.Video.
Breakpoints are yours
Section titled “Breakpoints are yours”Breakpoints read the window’s width. On a website that is the visitor’s browser; in a rendered class it is the frame’s own geometry, so the same ladder is legal there and means frame width — see A frame is a viewport below, and Frames and formats for the authoring discipline.
The kit declares no breakpoint and resets none of Tailwind’s. Where your design
swaps is your decision, so sm: md: lg: xl: 2xl: arrive at their standard
Tailwind values and nothing is switched off.
Name your own in theme.ts’s breakpoints group, beside your colours and type:
const theme = { // …colours, fonts, type, radii, shadows, layout… breakpoints: { phone: "390px", wide: "1440px", },};Each becomes a variant prefix — phone:flex, wide:grid-cols-3 — and they compile
into their own @theme block. Redefining one of Tailwind’s (md, lg) is fine; a
name that collides with a Tailwind state variant (hover, dark, print, …) is
rejected when your theme is validated, because the collision would otherwise silently
turn every existing hover: into a media query.
For a one-off threshold not worth naming, write the arbitrary variant inline:
min-[640px]:flex.
Stepping a theme token at a breakpoint: set it on body, not :root. Your
compiled theme declares its tokens in an unlayered :root block, and your hand-written
CSS is layered — so a :root { --tr-text-display: 72px } inside an @media in your
stylesheet loses to the theme’s value at every width, and the published page keeps the
base size. body has no unlayered rival and every section inherits from it.
A frame is a viewport
Section titled “A frame is a viewport”An image or document template renders into a frame, and every host owes a frame
document a window that is exactly the format’s bleed geometry. base.css’s
.tr-frame fills that window (width: 100vw; height: 100vh), so viewport units and
width/height/aspect-ratio @media read frame geometry by construction — they are the
primary responsive vocabulary, exactly as on a website. Container queries and
cqw/cqh inside a section are ordinary component styling, resolving against
containers the section declares itself.
A host that got that window wrong would leave you reading a plausible but
mis-measured frame, so base.css says so in the document: on a mismatch,
.tr-frame::after covers the frame with a diagnostic naming the window the
format declares and the window it was actually given. It is invisible whenever
the two agree. That pseudo-element and the --tr-frame-window-* custom
properties on the frame root belong to the kit — don’t repurpose either.
Two disciplines still hold for the rendered classes, graded over the compiled
stylesheet, so a Tailwind utility and a hand-written declaration are judged the same:
rendered-no-motion and
rendered-no-fixed. For a same-geometry destination
difference, reach for a per-format variant —
unknown-format-variant catches a variant prefix
naming no declared format, which would otherwise compile to nothing and say nothing.
See also
Section titled “See also”- Frames and formats — the authoring model: geometry-first styling, per-format variants, bleed and trim, and the format switcher.
- The theme model — why colour lives in tokens and why the cascade order is fixed rather than negotiated per section.
TokenTheme— the tokens your generatedtheme.csscarries.no-bare-css-import— why an@importinside a layered stylesheet is dropped.no-css-import-from-render-path— why a section does not import its own stylesheet.- Use
content-visibility— a hand-written rule placed into@layer template, and what containment costs.