Skip to content
AstroCraft Docs
On this theme

Layout & Spacing

Develi’s layout has one container width, seven breakpoints and a band rhythm that repeats on every page. Once you can see the rhythm, adding a section to an existing page is mostly a matter of deciding which band it belongs to.

The container

One class, in @layer components:

.site-container {
  @apply mx-auto max-w-[1100px] px-4;
}

1100px maximum, 16px gutters, centred. Every section that holds text or cards wraps its contents in it. Sections that need a full-bleed background put the background on the <section> and the container inside.

<section class="bg-card pt-20 pb-16 md:pt-24 md:pb-20">
  <div class="site-container">…</div>
</section>

Breakpoints

Declared explicitly in @theme rather than left implicit, with one addition:

--breakpoint-xs:  400px;   /* added */
--breakpoint-sm:  640px;
--breakpoint-md:  768px;
--breakpoint-lg:  1024px;
--breakpoint-xl:  1280px;
--breakpoint-2xl: 1536px;

xs is the only non-default. The rest mirror Tailwind’s own values and are written out so they are visible rather than assumed.

Everything is mobile-first: the base classes are the phone layout and each sm:/md:/lg: step widens it. The design is drawn at 1440, so in practice the mock’s own numbers are the md: or lg: values and the base is the scaled-down version.

The band rhythm

This is the structural idea worth understanding, because it is what makes the pages feel like one site rather than a stack of sections.

Every page alternates between two surfaces: the bg-background ground and a bg-card slab. A slab is opened and closed by a notch seam — a full-width band with a raised centre and concave shoulders, painted in the colour of the block on the other side of the join, so it reads as that block’s edge pushing into this one.

The home page has two slabs:

Hero            (background)  ─ bottom seam opens ─┐
About                                              │
Services                                    slab 1 │
CaseStudies     ─ top seam closes ─────────────────┘
Process         (background)  ─ bottom seam opens ─┐
Metrics                                            │
Testimonials                                slab 2 │
Stack                                              │
CallToAction    ─ top seam closes ─────────────────┘

The blog index and /work/ each have one slab. A case-study page has two.

Section order on every page follows the design file’s own y-order, and each route file carries a comment recording the exact y-coordinates so the next person can verify a section landed where the mock puts it rather than where it looked right.

SectionSeam

The seam is one component in Sections/Global/, and it is used at five boundaries:

<SectionSeam />                       <!-- fill-card, pointing down -->
<SectionSeam class="-scale-y-100" />  <!-- flipped -->

There is one shape, not two. Figma exports a separate vector for each edge, and the component first shipped carrying both — plus a path parser and a test asserting the two were exact mirrors. They were: reflecting every y about the box’s mid-line reproduces the other set byte for byte. So the second set was never a second shape, and -scale-y-100 says that in one class.

fill-card is the default because that is what all five call sites want. It is an ordinary class, so a caller needing a different surface overrides it through classtailwind-merge keeps the last fill-*.

preserveAspectRatio="none" plus a clamp() height is what keeps the shoulders at their designed fraction of the width at any viewport without dwarfing a phone.

The seams live on the sections, not in the route shell. That is what lets a page whose optional sections drop out still resolve its band structure — a case study with no gallery becomes one continuous slab, and nothing else has to change.

Two shared surface classes

Both are @apply classes in @layer components, and both exist because the same class string had grown independent copies across five unrelated components.

.recessed-panel

.recessed-panel {
  @apply bg-muted dark:bg-background border-border rounded-3xl border;
}

The deep surface drawn by anything sitting on a bg-card band: three of the seven cards, and both panels in the services tab rail.

The colour is a per-theme pair because the two ramps are not parallel. The band behind is --card, and one step down from it is --background in dark (a clear step) but base-50 on white in light, which is almost no step at all. Light’s deep surface is therefore --muted. Pairing them here means the two can no longer drift apart at one call site.

border-border is the divider token rather than any one mock’s literal grey — the home and services mocks bind two different greys for the same edge on the same card, which is one token doing one job, not two.

Every call site still sets its own padding and layout. Only the invariant lives in the class.

Two cards deliberately keep their own copy of the colour pair: NumberedCard and CaseCard expose it as one arm of a raised/recessed variant, a choice each call site makes rather than a fixed surface. Folding them in would delete that axis.

.ambient-glow

.ambient-glow {
  @apply bg-primary absolute rounded-[50%] opacity-30 blur-[150px] dark:opacity-40;
}

The terracotta wash behind the hero, the case rail, the CTA band and the footer reveal. Two things are baked in rather than left to call sites:

It is a real gaussian blur, not a radial-gradient. A gradient ramps alpha linearly where this has a long tail; side by side, the gradient reads as a contained blob rather than a wash.

The alpha is opacity, not a color-mix() in a gradient. Lightning CSS folds a color-mix over an unresolved var() down to the bare colour and silently drops the alpha — a bug that looks like the glow simply not working.

The per-theme opacity pair is needed because --primary is theme-invariant: one alpha cannot read the same over both grounds, since a warm halo over near-black turns a near-white panel peach.

Each call site still positions and sizes its own ellipse, because every node places a different one.

The header is fixed and out of flow

Header is a floating three-part bar — a bordered pill holding the logo and nav, then the theme toggle, then the CTA — pinned at top-6 with nothing behind it. Page content scrolls visibly through the gaps between the three chips. That is the design; the mock draws no scrolled state and no shadow, so neither is invented.

It retracts on scroll down and returns on scroll up. A fixed bar over a long page is a standing 68px tax on the reading area; giving it back while the reader moves forward and returning it the instant they reverse is the usual bargain. A small script toggles a data-hidden attribute and the element owns every pixel of the movement.

The translate is -100% - 1.5rem — its own height plus the gap it floats in. The 1.5rem must track top-6 above it, or the bar parks with a sliver still on screen. They are the same number written twice, because a translate percentage resolves against the element’s own box rather than the offset.

Two consequences for page layout:

  • scroll-padding-top: 5rem is set on html so in-page anchors do not land under the bar. Change the header’s height or offset and change this too.
  • Full-height centred sections sit under it deliberately — the hero and the 404. LegalArticle carries its own top padding to clear it, because a text page starting under the bar would be unreadable.

Vertical rhythm

There is no spacing scale beyond Tailwind’s own. Sections set their own padding, and the values that recur are pt-20 pb-16 on mobile stepping to md:pt-24 md:pb-20. Grid gaps run gap-6 for cards and gap-12/gap-16 for major columns.

Where a ratio is more robust than fixed widths, the theme uses one:

<div class="grid gap-12 lg:grid-cols-[minmax(0,1fr)_1.35fr] lg:items-start lg:gap-16">

minmax(0,1fr) on the first track rather than plain 1fr, so a long word in the copy cannot blow the grid out. That is a good default for any two-column text layout.

Adding a section to an existing page

  1. Decide which band it belongs to — is it on the ground or on a slab?
  2. If it is joining an existing slab, give it the same bg-card and no seam. Only the sections that open and close a slab draw one.
  3. Wrap its contents in .site-container unless it is deliberately full-bleed.
  4. Use the shared SectionHeading from Global/ rather than hand-writing a heading and lede — it owns the reveal delay that staggers the two, which is the drift it was extracted to end.
  5. Check the page in both themes and at sm, md and lg.
NEXT STEPUI Components