Layout & Spacing
Grafio has no Container component. Page width is two @apply classes in src/styles/global.css, applied directly to the elements that need them. That is a smaller mechanism than a wrapper component, and it means a section can put the frame on an inner row while keeping a full-bleed background — which the header does.
The two frames
.page-frame { @apply mx-auto w-full max-w-[90rem] px-5 md:px-10; }
.site-container { @apply mx-auto max-w-[1100px] px-4; }
.page-frame is the design frame: 90rem (1440px) max width with 20px gutters that step to 40px at md. It matches the Figma artboard, and it is what almost every section uses — 25 files, including the header and footer.
.site-container is the reading measure: 1100px, narrower, for pages where line length matters more than layout. Three files use it — the legal article, the 404 page and the UI catalog.
The distinction is content type, not page importance. If a section arranges things, it wants .page-frame. If a section is text somebody reads top to bottom, it wants .site-container.
Note what .page-frame frames: contents, not elements. The header keeps a full-width blurred backdrop and puts .page-frame on the row inside it, so the blur runs edge to edge while the logo and nav align with the page. Apply the same pattern for any section with a background that should bleed.
To change the site’s maximum width, edit one value in one of those two classes. Nothing else references a width.
Vertical rhythm
Sections use py-24 md:py-32 as the standard vertical rhythm — 96px stepping to 128px. Full-viewport sections like the home hero and the 404 use min-h-[100lvh] instead, and centre their content.
There is one clearance detail worth knowing: the header is fixed and overlays content. Sections that centre in the viewport are unaffected, but a section that starts at the top of the page needs padding to clear the bar. The legal article’s py-16 md:py-24 is doing exactly that — 64px against a roughly 62px header.
Breakpoints
Tailwind’s defaults, restated explicitly, plus one addition:
--breakpoint-xs: 400px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
xs exists for the narrowest phones. The theme is mobile-first throughout — base styles are the small-screen case and every breakpoint adds.
Where a layout splits from column to row is a measured decision, not a habit. The closing CTA band splits at lg rather than md because the pill takes about 227px plus a 64px gap, which would leave the headline 397px at md — too narrow for the line to hold.
The shared utility classes
Beyond the two frames and the type classes covered under Typography, three more are worth knowing:
@utility primary-focus {
@apply focus-visible:outline-primary-500 focus:outline-hidden
focus-visible:rounded-xs focus-visible:outline-2;
}
.main-text-gradient {
@apply from-primary-800 to-primary-600 dark:from-primary-400 dark:to-primary-200
bg-gradient-to-r bg-clip-text text-transparent;
}
primary-focus is the site’s focus ring. It is declared as an @utility rather than a class inside @layer utilities, and that difference is functional: a registered utility can be reached by @apply from any layer, which is how article.css gives article links the same ring. A plain class in the utilities layer cannot be applied from the components layer.
Keep a visible focus ring on anything interactive. Either use primary-focus or the focus-visible:ring-3 that the primitives already carry.
The layer order
global.css declares the cascade order explicitly, which is what makes @apply predictable:
@layer theme, base, components, utilities;
The full import graph, starting at the single entry point that BaseLayout imports:
src/styles/global.css
├── fonts.css two @font-face declarations
├── tailwindcss the framework
├── @variant dark, @variant js
├── tailwind-theme.css palette aliases + the @theme inline bridge
├── motion/index.css the animate-* catalog
│ └── motion/keyframes.css
├── @layer theme, base, components, utilities
├── article.css (into layer: components)
├── @layer base :root, .dark, element defaults
├── @layer components .h1 .h2 .h3 .display-heading .description .site-container .page-frame
├── @utility primary-focus
└── @layer utilities .main-text-gradient
Two more stylesheets sit outside this graph, imported from component frontmatter and deliberately unlayered so they outrank utilities: ui/_overlay.css (dialog and sheet transitions, plus the scroll lock) and ui/accordion/_accordion.css.
When to reach for @apply
The house rule: @apply is for a pattern repeated across many unrelated elements. .h2 earns it — thirty files use it. .page-frame earns it — twenty-five do.
Reach for a component or a tv() variant config when the thing has structure or variants. Do not @apply to tidy a one-off class list; that just moves the mess somewhere harder to find.
The smell test: six or more utilities with a ternary inside a class attribute means it should be a tv() component. The same cluster of classes appearing three or more times means it should be a component or an @apply class.
Class ordering
prettier-plugin-tailwindcss orders utility classes, and it must be the last plugin in .prettierrc.mjs. Run pnpm format and let it sort. Hand-ordering class lists is work the tool does better, and fighting it produces diffs nobody wants to review.
Overriding a primitive’s layout
Every primitive merges a consumer class through its tv() config, and tv() runs tailwind-merge, so the last conflicting utility wins:
<Card variant="interactive" class="max-w-sm sm:flex-row">
That is how the shipped code makes a horizontal card, sets a status accent (class="border-t-primary border-t-4"), or constrains a width. A one-off class, not a new variant — variants are for arrangements the design uses repeatedly.
One trap: because the consumer class merges last, putting a colour into an override can beat a state the primitive sets. The contact form’s underline-style fields carry a shape override deliberately free of colour, so that the border-error applied by state="error" still wins. Style shape in your override; leave colour to the primitive.