Pulliv0.1
8px
03 — Lattice

Two numbers, one geometry.

The lattice is not a grid overlay you align to. It is the generating function for every dimension in the system. This page is the derivation, followed by an honest account of the four places it deliberately does not apply.

The meeting condition

Why the loop radius is exactly half the pitch, and why that is not a stylistic choice.

Dots sit on a square lattice at pitch P. The kolam line must curve around each dot without touching it, at some radius r. Consider two adjacent dots and the loops that pass between them.

If r < P/2, the two loops fail to reach each other and the line has a gap — it breaks, which the form forbids. If r > P/2, the loops overlap and the line crosses itself where it should not. Only at r = P/2 do the two arcs meet exactly once, at the midpoint of the lattice edge between the dots — and there their tangents are both perpendicular to that edge, so they join smoothly.

That is the whole geometry. Because the arcs meet at edge midpoints with matching tangents, any tile can sit next to any other tile and the line stays continuous. The pattern engine is only possible because of it, and so is the token model: fixing r in terms of P fixes the radius scale to the spacing scale forever.

A 5×5 kolam with no crossings — every curve is a quarter arc of radius P/2 centred on a dot.

Consequence. A corner radius of --valai-2 is not "8px because that looked right". It is one loop radius — the same curve the generator draws around a dot. Increase the pitch and the corner grows in lockstep with the padding, because they are the same number scaled.

Spacing

Orthogonal lattice steps only. A half-step would land the line on a dot, which a kolam line may never do — so the scale has no half-steps either.

TokenDerivationValueScale

Three spacing roles

Primitives are magnitudes; roles say what a magnitude is for. Product code uses roles, never primitives — that is what lets the primitive change without a rewrite.

inset · inside a box

  • inset-xs1 P
  • inset-sm2 P
  • inset-md3 P
  • inset-lg4 P
  • inset-xl6 P

Card padding, panel padding, button padding.

stack · between rows

  • stack-tight1 P
  • stack2 P
  • stack-loose4 P
  • section12 P

Vertical rhythm. Label to input is tight; section to section is 12 P.

inline · between siblings

  • inline2 P
  • gutter3 P
  • page-margin4 P

Button groups, grid gutters, page edges.

Rule of proximity. The gap between related things must be smaller than the gap to the next group, by at least one full step. If label-to-input is 1 P, then field-to-field is at least 3 P. Adjacent steps are not enough separation to read as a grouping.

Sizing

Radii, stroke weights, and control heights.

valai · radius

TokenDerivationValueCorner

kambi · stroke

TokenUseValueWeight

Three weights is the maximum a drawn system can justify. Rice flour falls at one thickness; the variation here is hairline, edge, and the drawn line itself.

alavu · control heights

Derived from the lattice, then floored at 44px. The floor is the system's one legitimate override and is documented as such — see where derivation breaks.

TokenDerivationAt P=8At P=6Used by
--alavu-xs3 P24px18pxBadges, tags — non-interactive only
--alavu-sm4 P32px24pxDense toolbars, table row actions
--alavu-mdmax(5.5 P, 44px)44px44px flooredDefault control height
--alavu-lgmax(7 P, 48px)56px48px flooredPrimary CTA, mobile-first forms
xs and sm are not touch targets. They may carry a hit area larger than their visual box, but a --alavu-sm control on a touch surface must be padded out to 44px of interactive area or it fails WCAG 2.5.8. The visual size and the target size are different measurements and the system does not conflate them.

Layout grid

Twelve columns, lattice gutters, mirrored margins — the default symmetry from principle 03 expressed as page structure.

  • Columns12
  • Gutter3 P
  • Page margin (≥ lg)4 P
  • Page margin (< md)2 P
  • Max content width176 P
  • Prose measure72 ch
  • Sidebar width30 P

Why twelve. Twelve divides by 2, 3, 4 and 6, which covers every column split a content page needs without introducing a second grid for exceptions. Nothing kolam-derived about it — and the system says so rather than inventing a justification.

Why 72ch. Measure is governed by reading, not by the lattice. Line length is a typographic constraint and belongs to the type layer.

Breakpoints

Breakpoints are viewport facts, not lattice facts — they do not scale with pitch. They were chosen as lattice multiples at P=8 so the numbers stay familiar, and then fixed.

NameWidthAs lattice (P=8)Layout change
sm320px40 PSingle column, 2 P margins, stacked controls
md480px60 PTwo-column forms, inline button groups
lg768px96 PSidebar appears, 4 P margins, 12-col grid active
xl1024px128 PPersistent navigation, three-column content
2xl1280px160 PMax width engages, margins become auto

Density tiers

Kolams are drawn on 3, 5, 7, 9 dot grids. The same recursion gives the density tiers — one attribute, whole interface re-derived.

compact · P = 6

Dense tables, admin consoles, monitoring dashboards. Controls hit the 44px floor, so the visual rhythm tightens while target sizes hold.

default · P = 8

The reference tier. Every published token value on this site is quoted at P = 8 unless stated otherwise.

comfortable · P = 10

Touch-first surfaces, onboarding, marketing pages, accessibility-forward configurations.

<!-- one attribute; nothing else in the app changes -->
<html data-density="compact">

/* tokens.css */
:root[data-density="compact"]     { --p: 6px; }
:root[data-density="default"]     { --p: 8px; }
:root[data-density="comfortable"] { --p: 10px; }
Type does not change with density. Only space and geometry follow the pitch. A compact table is tighter, not smaller to read — which is the failure mode of most density switches.

Elevation

A kolam is flour lying flat on a floor. It casts no shadow. Depth here is expressed by lifting the ground, not by casting light.

Most systems have five or six elevation levels and use them to signal importance, which quietly turns shadow into a second, unowned hierarchy. Pulli has three, and only the third is a shadow of any consequence.

Surface stepping does the work. Depth is primarily communicated by moving between --color-surface-sunken, --color-bg and --color-surface. Shadow is reserved for things that genuinely float above the page and must be dismissible.

  • --elevation-raisedhairline shadow
  • --elevation-panelcards, popovers
  • --elevation-overlaymodal, toast only
raised
panel
overlay

Where derivation breaks

Four places the lattice does not govern. A system that hides its exceptions teaches people to make undocumented ones.

ExceptionGoverned byRule
Target sizeWCAG 2.5.8 Interactive controls take an absolute 44px floor via max(). At P = 6 the derived height would be 33px. The floor wins, always, and is written into the token rather than patched per component.
Hairlinesdevice pixels --kambi-1 is a literal 1px, not r/4. Sub-pixel borders render inconsistently across devices and browsers. A hairline is a rendering primitive, not a lattice measurement.
Type scalethe diagonal, √2 Type does not scale with pitch. The lattice governs space and geometry; the lattice diagonal governs proportion. Coupling type to density makes compact modes unreadable rather than dense.
Breakpoints & measureviewport and reading Breakpoints are properties of devices, and line length is a property of reading. Both are fixed. Deriving them from the pitch would be numerology, not design.
How to add an exception. An exception must be argued in a pull request, land in tokens.css with an inline comment naming the standard or constraint that forced it, and be listed in this table. An exception that lives in component code instead is a bug, however reasonable it looked at the time.